new translation labels & Portuguese from Portugal translation

This commit is contained in:
André Glatzl
2020-11-11 22:17:36 +01:00
parent 8d883f89eb
commit 3d1f54f62f
32 changed files with 802 additions and 303 deletions

View File

@@ -4,6 +4,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { EditIcon } from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import styled from "styled-components";
import User from "models/User";
import UserProfile from "scenes/UserProfile";
@@ -18,6 +19,7 @@ type Props = {
lastViewedAt: string,
};
@withTranslation()
@observer
class AvatarWithPresence extends React.Component<Props> {
@observable isOpen: boolean = false;
@@ -37,6 +39,7 @@ class AvatarWithPresence extends React.Component<Props> {
isPresent,
isEditing,
isCurrentUser,
t,
} = this.props;
return (
@@ -44,13 +47,20 @@ class AvatarWithPresence extends React.Component<Props> {
<Tooltip
tooltip={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && "(You)"}
<br />
{isPresent
? isEditing
? "currently editing"
: "currently viewing"
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
{t(
"<strong>{{ userName }} {{ you }} <br /> {{ action }}</strong>",
{
userName: user.name,
you: isCurrentUser && t("(You)"),
action: isPresent
? isEditing
? t("currently editing")
: t("currently viewing")
: t("viewed {{ timeAgo }} ago", {
timeAgo: distanceInWordsToNow(new Date(lastViewedAt)),
}),
}
)}
</Centered>
}
placement="bottom"

View File

@@ -1,4 +1,5 @@
// @flow
import i18n from "i18next";
import { observer, inject } from "mobx-react";
import {
ArchiveIcon,
@@ -21,6 +22,8 @@ import Flex from "components/Flex";
import BreadcrumbMenu from "./BreadcrumbMenu";
import { collectionUrl } from "utils/routeHelpers";
const t = (k) => i18n.t(k);
type Props = {
document: Document,
collections: CollectionsStore,
@@ -34,7 +37,7 @@ function Icon({ document }) {
<CollectionName to="/trash">
<TrashIcon color="currentColor" />
&nbsp;
<span>Trash</span>
<span>{t("Trash")}</span>
</CollectionName>
<Slash />
</>
@@ -46,7 +49,7 @@ function Icon({ document }) {
<CollectionName to="/archive">
<ArchiveIcon color="currentColor" />
&nbsp;
<span>Archive</span>
<span>{t("Archive")}</span>
</CollectionName>
<Slash />
</>
@@ -58,7 +61,7 @@ function Icon({ document }) {
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
<span>{t("Drafts")}</span>
</CollectionName>
<Slash />
</>
@@ -70,7 +73,7 @@ function Icon({ document }) {
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
<span>{t("Templates")}</span>
</CollectionName>
<Slash />
</>

View File

@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { StarredIcon, PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { Link, Redirect } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Document from "models/Document";
@@ -29,6 +30,7 @@ type Props = {
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@withTranslation()
@observer
class DocumentPreview extends React.Component<Props> {
@observable redirectTo: ?string;
@@ -72,6 +74,7 @@ class DocumentPreview extends React.Component<Props> {
showTemplate,
highlight,
context,
t,
} = this.props;
if (this.redirectTo) {
@@ -91,7 +94,7 @@ class DocumentPreview extends React.Component<Props> {
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
{document.isNew && <Badge yellow>New</Badge>}
{document.isNew && <Badge yellow>{t("New")}</Badge>}
{!document.isDraft &&
!document.isArchived &&
!document.isTemplate && (
@@ -104,12 +107,16 @@ class DocumentPreview extends React.Component<Props> {
</Actions>
)}
{document.isDraft && showDraft && (
<Tooltip tooltip="Only visible to you" delay={500} placement="top">
<Badge>Draft</Badge>
<Tooltip
tooltip={t("Only visible to you")}
delay={500}
placement="top"
>
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{document.isTemplate && showTemplate && (
<Badge primary>Template</Badge>
<Badge primary>{t("Template")}</Badge>
)}
<SecondaryActions>
{document.isTemplate &&
@@ -120,7 +127,7 @@ class DocumentPreview extends React.Component<Props> {
icon={<PlusIcon />}
neutral
>
New doc
{t("New doc")}
</Button>
)}
&nbsp;

View File

@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { MoreIcon } from "outline-icons";
import { rgba } from "polished";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { PortalWithState } from "react-portal";
import styled from "styled-components";
import { fadeAndScaleIn } from "shared/styles/animations";
@@ -29,6 +30,7 @@ type Props = {
position?: "left" | "right" | "center",
};
@withTranslation()
@observer
class DropdownMenu extends React.Component<Props> {
id: string = `menu${counter++}`;
@@ -51,8 +53,9 @@ class DropdownMenu extends React.Component<Props> {
) => {
return (ev: SyntheticMouseEvent<HTMLElement>) => {
ev.preventDefault();
const { t } = this.props;
const currentTarget = ev.currentTarget;
invariant(document.body, "why you not here");
invariant(document.body, t("why you not here"));
if (currentTarget instanceof HTMLDivElement) {
this.bodyRect = document.body.getBoundingClientRect();
@@ -150,7 +153,7 @@ class DropdownMenu extends React.Component<Props> {
};
render() {
const { className, hover, label, children } = this.props;
const { className, hover, label, children, t } = this.props;
return (
<div className={className}>
@@ -177,7 +180,7 @@ class DropdownMenu extends React.Component<Props> {
{label || (
<NudeButton
id={`${this.id}button`}
aria-label="More options"
aria-label={t("More options")}
aria-haspopup="true"
aria-expanded={isOpen ? "true" : "false"}
aria-controls={this.id}

View File

@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { SearchIcon } from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
@@ -18,6 +19,7 @@ type Props = {
collectionId?: string,
};
@withTranslation()
@observer
class InputSearch extends React.Component<Props> {
input: ?Input;
@@ -51,13 +53,13 @@ class InputSearch extends React.Component<Props> {
};
render() {
const { theme, placeholder = "Search…" } = this.props;
const { theme, t } = this.props;
return (
<InputMaxWidth
ref={(ref) => (this.input = ref)}
type="search"
placeholder={placeholder}
placeholder={t("Search...")}
onInput={this.handleSearchInput}
icon={
<SearchIcon

View File

@@ -12,6 +12,7 @@ import {
PlusIcon,
} from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
@@ -36,6 +37,7 @@ type Props = {
policies: PoliciesStore,
};
@withTranslation()
@observer
class MainSidebar extends React.Component<Props> {
@observable inviteModalOpen = false;
@@ -65,7 +67,7 @@ class MainSidebar extends React.Component<Props> {
};
render() {
const { auth, documents, policies } = this.props;
const { auth, documents, policies, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
@@ -90,7 +92,7 @@ class MainSidebar extends React.Component<Props> {
to="/home"
icon={<HomeIcon color="currentColor" />}
exact={false}
label="Home"
label={t("Home")}
/>
<SidebarLink
to={{
@@ -98,20 +100,20 @@ class MainSidebar extends React.Component<Props> {
state: { fromMenu: true },
}}
icon={<SearchIcon color="currentColor" />}
label="Search"
label={t("Search")}
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon color="currentColor" />}
exact={false}
label="Starred"
label={t("Starred")}
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label="Templates"
label={t("Templates")}
active={
documents.active ? documents.active.template : undefined
}
@@ -121,7 +123,7 @@ class MainSidebar extends React.Component<Props> {
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
Drafts
{t("Drafts")}
{documents.totalDrafts > 0 && (
<Bubble count={documents.totalDrafts} />
)}
@@ -146,7 +148,7 @@ class MainSidebar extends React.Component<Props> {
to="/archive"
icon={<ArchiveIcon color="currentColor" />}
exact={false}
label="Archive"
label={t("Archive")}
active={
documents.active
? documents.active.isArchived && !documents.active.isDeleted
@@ -157,7 +159,7 @@ class MainSidebar extends React.Component<Props> {
to="/trash"
icon={<TrashIcon color="currentColor" />}
exact={false}
label="Trash"
label={t("Trash")}
active={
documents.active ? documents.active.isDeleted : undefined
}
@@ -167,21 +169,21 @@ class MainSidebar extends React.Component<Props> {
to="/settings/people"
onClick={this.handleInviteModalOpen}
icon={<PlusIcon color="currentColor" />}
label="Invite people…"
label={t("Invite people…")}
/>
)}
</Section>
</Scrollable>
</Flex>
<Modal
title="Invite people"
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
<Modal
title="Create a collection"
title={t("Create a collection")}
onRequestClose={this.handleCreateCollectionModalClose}
isOpen={this.createCollectionModalOpen}
>

View File

@@ -2,6 +2,7 @@
import { observer, inject } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
@@ -26,6 +27,7 @@ type Props = {
ui: UiStore,
};
@withTranslation()
@observer
class Collections extends React.Component<Props> {
isPreloaded: boolean = !!this.props.collections.orderedData.length;
@@ -52,7 +54,7 @@ class Collections extends React.Component<Props> {
}
render() {
const { collections, ui, policies, documents } = this.props;
const { collections, ui, policies, documents, t } = this.props;
const content = (
<>
@@ -71,7 +73,7 @@ class Collections extends React.Component<Props> {
to="/collections"
onClick={this.props.onCreateCollection}
icon={<PlusIcon color="currentColor" />}
label="New collection…"
label={t("New collection…")}
exact
/>
</>
@@ -79,7 +81,7 @@ class Collections extends React.Component<Props> {
return (
<Flex column>
<Header>Collections</Header>
<Header>{t("Collections")}</Header>
{collections.isLoaded ? (
this.isPreloaded ? (
content

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Collection from "models/Collection";
@@ -25,6 +26,7 @@ type Props = {|
depth: number,
|};
@withTranslation()
@observer
class DocumentLink extends React.Component<Props> {
@observable menuOpen = false;
@@ -84,6 +86,7 @@ class DocumentLink extends React.Component<Props> {
prefetchDocument,
depth,
canUpdate,
t,
} = this.props;
const showChildren = !!(
@@ -96,7 +99,7 @@ class DocumentLink extends React.Component<Props> {
this.isActiveDocument())
);
const document = documents.get(node.id);
const title = node.title || "Untitled";
const title = node.title || t("Untitled");
return (
<Flex

View File

@@ -6,6 +6,7 @@ import * as React from "react";
import { render } from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { outlineTranslation } from "shared/translations/i18n";
import stores from "stores";
import ErrorBoundary from "components/ErrorBoundary";
import ScrollToTop from "components/ScrollToTop";
@@ -14,8 +15,6 @@ import Toasts from "components/Toasts";
import Routes from "./routes";
import env from "env";
import { outlineTranslation } from "shared/translations/i18n";
outlineTranslation.init();
const element = document.getElementById("root");

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 } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
@@ -25,6 +26,7 @@ type Props = {
auth: AuthStore,
};
@withTranslation()
@observer
class AccountMenu extends React.Component<Props> {
@observable keyboardShortcutsOpen: boolean = false;
@@ -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>
</>

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
@@ -28,6 +29,7 @@ type Props = {
onClose?: () => void,
};
@withTranslation()
@observer
class CollectionMenu extends React.Component<Props> {
file: ?HTMLInputElement;
@@ -111,6 +113,7 @@ class CollectionMenu extends React.Component<Props> {
position,
onOpen,
onClose,
t,
} = this.props;
const can = policies.abilities(collection.id);
@@ -127,7 +130,7 @@ class CollectionMenu extends React.Component<Props> {
</VisuallyHidden>
<Modal
title="Collection permissions"
title={t("Collection permissions")}
onRequestClose={this.handleMembersModalClose}
isOpen={this.showCollectionMembers}
>
@@ -143,40 +146,40 @@ class CollectionMenu extends React.Component<Props> {
<>
{can.update && (
<DropdownMenuItem onClick={this.onNewDocument}>
New document
{t("New document")}
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.onImportDocument}>
Import document
{t("Import document")}
</DropdownMenuItem>
)}
{can.update && <hr />}
{can.update && (
<DropdownMenuItem onClick={this.handleEditCollectionOpen}>
Edit
{t("Edit…")}
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.handleMembersModalOpen}>
Permissions
{t("Permissions…")}
</DropdownMenuItem>
)}
{can.export && (
<DropdownMenuItem onClick={this.handleExportCollectionOpen}>
Export
{t("Export…")}
</DropdownMenuItem>
)}
</>
)}
{can.delete && (
<DropdownMenuItem onClick={this.handleDeleteCollectionOpen}>
Delete
{t("Delete…")}
</DropdownMenuItem>
)}
</DropdownMenu>
<Modal
title="Edit collection"
title={t("Edit collection")}
isOpen={this.showCollectionEdit}
onRequestClose={this.handleEditCollectionClose}
>
@@ -186,7 +189,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Delete collection"
title={t("Delete collection")}
isOpen={this.showCollectionDelete}
onRequestClose={this.handleDeleteCollectionClose}
>
@@ -196,7 +199,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Export collection"
title={t("Export collection")}
isOpen={this.showCollectionExport}
onRequestClose={this.handleExportCollectionClose}
>

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import AuthStore from "stores/AuthStore";
import CollectionStore from "stores/CollectionsStore";
@@ -43,6 +44,7 @@ type Props = {
onClose?: () => void,
};
@withTranslation()
@observer
class DocumentMenu extends React.Component<Props> {
@observable redirectTo: ?string;
@@ -86,7 +88,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 = () => {
@@ -103,7 +106,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 (
@@ -111,12 +115,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<>) => {
@@ -167,6 +173,7 @@ class DocumentMenu extends React.Component<Props> {
label,
onOpen,
onClose,
t,
} = this.props;
const can = policies.abilities(document.id);
@@ -185,17 +192,17 @@ class DocumentMenu extends React.Component<Props> {
>
{can.unarchive && (
<DropdownMenuItem onClick={this.handleRestore}>
Restore
{t("Restore")}
</DropdownMenuItem>
)}
{can.restore &&
(collection ? (
<DropdownMenuItem onClick={this.handleRestore}>
Restore
{t("Restore")}
</DropdownMenuItem>
) : (
<DropdownMenu
label={<DropdownMenuItem>Restore</DropdownMenuItem>}
label={<DropdownMenuItem>{t("Restore…")}</DropdownMenuItem>}
style={{
left: -170,
position: "relative",
@@ -203,7 +210,7 @@ class DocumentMenu extends React.Component<Props> {
}}
hover
>
<Header>Choose a collection</Header>
<Header>{t("Choose a collection")}</Header>
{collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
@@ -226,42 +233,42 @@ class DocumentMenu extends React.Component<Props> {
(document.pinned
? can.unpin && (
<DropdownMenuItem onClick={this.handleUnpin}>
Unpin
{t("Unpin")}
</DropdownMenuItem>
)
: can.pin && (
<DropdownMenuItem onClick={this.handlePin}>
Pin to collection
{t("Pin to collection")}
</DropdownMenuItem>
))}
{document.isStarred
? can.unstar && (
<DropdownMenuItem onClick={this.handleUnstar}>
Unstar
{t("Unstar")}
</DropdownMenuItem>
)
: can.star && (
<DropdownMenuItem onClick={this.handleStar}>
Star
{t("Star")}
</DropdownMenuItem>
)}
{canShareDocuments && (
<DropdownMenuItem
onClick={this.handleShareLink}
title="Create a public share link"
title={t("Create a public share link")}
>
Share link
{t("Share link…")}
</DropdownMenuItem>
)}
{showToggleEmbeds && (
<>
{document.embedsDisabled ? (
<DropdownMenuItem onClick={document.enableEmbeds}>
Enable embeds
{t("Enable embeds")}
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={document.disableEmbeds}>
Disable embeds
{t("Disable embeds")}
</DropdownMenuItem>
)}
</>
@@ -271,61 +278,69 @@ class DocumentMenu extends React.Component<Props> {
{can.createChildDocument && (
<DropdownMenuItem
onClick={this.handleNewChild}
title="Create a nested document inside the current document"
title={t("Create a nested document inside the current document")}
>
New nested document
{t("New nested document")}
</DropdownMenuItem>
)}
{can.update && !document.isTemplate && (
<DropdownMenuItem onClick={this.handleOpenTemplateModal}>
Create template
{t("Create template…")}
</DropdownMenuItem>
)}
{can.unpublish && (
<DropdownMenuItem onClick={this.handleUnpublish}>
Unpublish
{t("Unpublish")}
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.handleEdit}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleEdit}>
{t("Edit")}
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.handleDuplicate}>
Duplicate
{t("Duplicate")}
</DropdownMenuItem>
)}
{can.archive && (
<DropdownMenuItem onClick={this.handleArchive}>
Archive
{t("Archive")}
</DropdownMenuItem>
)}
{can.delete && (
<DropdownMenuItem onClick={this.handleDelete}>
Delete
{t("Delete…")}
</DropdownMenuItem>
)}
{can.move && (
<DropdownMenuItem onClick={this.handleMove}>Move</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleMove}>
{t("Move…")}
</DropdownMenuItem>
)}
<hr />
{canViewHistory && (
<>
<DropdownMenuItem onClick={this.handleDocumentHistory}>
History
{t("History")}
</DropdownMenuItem>
</>
)}
{can.download && (
<DropdownMenuItem onClick={this.handleExport}>
Download
{t("Download")}
</DropdownMenuItem>
)}
{showPrint && (
<DropdownMenuItem onClick={window.print}>Print</DropdownMenuItem>
<DropdownMenuItem onClick={window.print}>
{t("Print")}
</DropdownMenuItem>
)}
</DropdownMenu>
<Modal
title={`Delete ${this.props.document.noun}`}
title={t("Delete {{ documentName }}", {
documentName: this.props.document.noun,
})}
onRequestClose={this.handleCloseDeleteModal}
isOpen={this.showDeleteModal}
>
@@ -335,7 +350,7 @@ class DocumentMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Create template"
title={t("Create template")}
onRequestClose={this.handleCloseTemplateModal}
isOpen={this.showTemplateModal}
>
@@ -345,7 +360,7 @@ class DocumentMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Share document"
title={t("Share document")}
onRequestClose={this.handleCloseShareModal}
isOpen={this.showShareModal}
>

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
@@ -22,6 +23,7 @@ type Props = {
onClose?: () => void,
};
@withTranslation()
@observer
class GroupMenu extends React.Component<Props> {
@observable editModalOpen: boolean = false;
@@ -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}
>
@@ -77,18 +79,20 @@ class GroupMenu extends React.Component<Props> {
{group && (
<>
<DropdownMenuItem onClick={this.props.onMembers}>
Members
{t("Members…")}
</DropdownMenuItem>
{(can.update || can.delete) && <hr />}
{can.update && (
<DropdownMenuItem onClick={this.onEdit}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={this.onEdit}>
{t("Edit…")}
</DropdownMenuItem>
)}
{can.delete && (
<DropdownMenuItem onClick={this.onDelete}>
Delete
{t("Delete…")}
</DropdownMenuItem>
)}
</>

View File

@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -16,6 +17,7 @@ type Props = {
collections: CollectionsStore,
};
@withTranslation()
@observer
class NewChildDocumentMenu extends React.Component<Props> {
@observable redirectTo: ?string;
@@ -39,19 +41,19 @@ class NewChildDocumentMenu extends React.Component<Props> {
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { label, document, collections, ...rest } = this.props;
const { label, document, collections, t, ...rest } = this.props;
const collection = collections.get(document.collectionId);
return (
<DropdownMenu label={label || <MoreIcon />} {...rest}>
<DropdownMenuItem onClick={this.handleNewDocument}>
<span>
New document in{" "}
<strong>{collection ? collection.name : "collection"}</strong>
{t("New document in")}{" "}
<strong>{collection ? collection.name : t("collection")}</strong>
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleNewChild}>
New nested document
{t("New nested document")}
</DropdownMenuItem>
</DropdownMenu>
);

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 } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -24,6 +25,7 @@ type Props = {
policies: PoliciesStore,
};
@withTranslation()
@observer
class NewDocumentMenu extends React.Component<Props> {
@observable redirectTo: ?string;
@@ -47,7 +49,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 (
@@ -55,14 +57,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>
{collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);

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 } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -22,6 +23,7 @@ type Props = {
policies: PoliciesStore,
};
@withTranslation()
@observer
class NewTemplateMenu extends React.Component<Props> {
@observable redirectTo: ?string;
@@ -39,20 +41,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>
{collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);

View File

@@ -1,6 +1,7 @@
// @flow
import { inject } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import UiStore from "stores/UiStore";
@@ -21,20 +22,23 @@ type Props = {
ui: UiStore,
};
@withTranslation()
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,11 +52,11 @@ 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>
);

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import SharesStore from "stores/SharesStore";
@@ -18,6 +19,7 @@ type Props = {
share: Share,
};
@withTranslation()
@observer
class ShareMenu extends React.Component<Props> {
@observable redirectTo: ?string;
@@ -36,32 +38,34 @@ 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>
);

View File

@@ -2,6 +2,7 @@
import { observer, inject } from "mobx-react";
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Document from "models/Document";
@@ -13,10 +14,11 @@ type Props = {
documents: DocumentsStore,
};
@withTranslation()
@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,10 @@ 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>
))}

View File

@@ -2,6 +2,7 @@
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import UsersStore from "stores/UsersStore";
import User from "models/User";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
@@ -11,14 +12,18 @@ type Props = {
users: UsersStore,
};
@withTranslation()
@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 want to make {{ userName }} an admin? Admins can modify team and billing information.",
{ userName: user.name }
)
)
) {
return;
@@ -28,8 +33,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 want to make {{ userName }} a member?", {
userName: user.name,
})
)
) {
return;
}
users.demote(user);
@@ -37,10 +48,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 want to suspend this account? Suspended users will be prevented from logging in."
)
)
) {
return;
@@ -61,33 +74,33 @@ class UserMenu extends React.Component<Props> {
};
render() {
const { user } = this.props;
const { user, t } = this.props;
return (
<DropdownMenu>
{user.isAdmin && (
<DropdownMenuItem onClick={this.handleDemote}>
Make {user.name} a member
{t("Make {{ userName }} a member…", { userName: user.name })}
</DropdownMenuItem>
)}
{!user.isAdmin && !user.isSuspended && (
<DropdownMenuItem onClick={this.handlePromote}>
Make {user.name} an admin
{t("Make {{ userName }} an admin…", { userName: user.name })}
</DropdownMenuItem>
)}
{!user.lastActiveAt && (
<DropdownMenuItem onClick={this.handleRevoke}>
Revoke invite
{t("Revoke invite…")}
</DropdownMenuItem>
)}
{user.lastActiveAt &&
(user.isSuspended ? (
<DropdownMenuItem onClick={this.handleActivate}>
Activate account
{t("Activate account")}
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={this.handleSuspend}>
Suspend account
{t("Suspend account…")}
</DropdownMenuItem>
))}
</DropdownMenu>

View File

@@ -4,6 +4,7 @@ import { observer, inject } from "mobx-react";
import { NewDocumentIcon, PlusIcon, PinIcon } from "outline-icons";
import * as React from "react";
import { withTranslation } from "react-i18next";
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
import styled, { withTheme } from "styled-components";
@@ -49,6 +50,7 @@ type Props = {
theme: Theme,
};
@withTranslation()
@observer
class CollectionScene extends React.Component<Props> {
@observable collection: ?Collection;
@@ -132,7 +134,7 @@ class CollectionScene extends React.Component<Props> {
};
renderActions() {
const { match, policies } = this.props;
const { match, policies, t } = this.props;
const can = policies.abilities(match.params.id || "");
return (
@@ -142,19 +144,19 @@ class CollectionScene extends React.Component<Props> {
<Action>
<InputSearch
source="collection"
placeholder="Search in collection…"
placeholder={t("Search in collection…")}
collectionId={match.params.id}
/>
</Action>
<Action>
<Tooltip
tooltip="New document"
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button onClick={this.onNewDocument} icon={<PlusIcon />}>
New doc
{t("New doc")}
</Button>
</Tooltip>
</Action>
@@ -169,7 +171,7 @@ class CollectionScene extends React.Component<Props> {
}
render() {
const { documents, theme } = this.props;
const { documents, theme, t } = this.props;
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
if (!this.isFetching && !this.collection) return <Search notFound />;
@@ -188,26 +190,31 @@ class CollectionScene extends React.Component<Props> {
{collection.isEmpty ? (
<Centered column>
<HelpText>
<strong>{collection.name}</strong> doesnt contain any
documents yet.
<br />
Get started by creating a new one!
{t(
`
<strong>{{ collectionName }}</strong> doesnt contain any
documents yet.
<br />
Get started by creating a new one!
`,
{ collectionName: collection.name }
)}
</HelpText>
<Wrapper>
<Link to={newDocumentUrl(collection.id)}>
<Button icon={<NewDocumentIcon color={theme.buttonText} />}>
Create a document
{t("Create a document")}
</Button>
</Link>
&nbsp;&nbsp;
{collection.private && (
<Button onClick={this.onPermissions} neutral>
Manage members
{t("Manage members…")}
</Button>
)}
</Wrapper>
<Modal
title="Collection permissions"
title={t("Collection permissions")}
onRequestClose={this.handlePermissionsModalClose}
isOpen={this.permissionsModalOpen}
>
@@ -218,7 +225,7 @@ class CollectionScene extends React.Component<Props> {
/>
</Modal>
<Modal
title="Edit collection"
title={t("Edit collection")}
onRequestClose={this.handleEditModalClose}
isOpen={this.editModalOpen}
>
@@ -249,7 +256,7 @@ class CollectionScene extends React.Component<Props> {
{hasPinnedDocuments && (
<>
<Subheading>
<TinyPinIcon size={18} /> Pinned
<TinyPinIcon size={18} /> {t("Pinned")}
</Subheading>
<DocumentList documents={pinnedDocuments} showPin />
</>
@@ -257,16 +264,16 @@ class CollectionScene extends React.Component<Props> {
<Tabs>
<Tab to={collectionUrl(collection.id)} exact>
Recently updated
{t("Recently updated")}
</Tab>
<Tab to={collectionUrl(collection.id, "recent")} exact>
Recently published
{t("Recently published")}
</Tab>
<Tab to={collectionUrl(collection.id, "old")} exact>
Least recently updated
{t("Least recently updated")}
</Tab>
<Tab to={collectionUrl(collection.id, "alphabetical")} exact>
AZ
{t("AZ")}
</Tab>
</Tabs>
<Switch>

View File

@@ -3,6 +3,7 @@ import { debounce } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore";
@@ -28,6 +29,7 @@ type Props = {
onSubmit: () => void,
};
@withTranslation()
@observer
class AddGroupsToCollection extends React.Component<Props> {
@observable newGroupModalOpen: boolean = false;
@@ -53,49 +55,54 @@ class AddGroupsToCollection extends React.Component<Props> {
}, 250);
handleAddGroup = (group) => {
const { t } = this.props;
try {
this.props.collectionGroupMemberships.create({
collectionId: this.props.collection.id,
groupId: group.id,
permission: "read_write",
});
this.props.ui.showToast(`${group.name} was added to the collection`);
this.props.ui.showToast(
t("{{ groupName }} was added to the collection", {
groupName: group.name,
})
);
} catch (err) {
this.props.ui.showToast("Could not add user");
this.props.ui.showToast(t("Could not add user"));
console.error(err);
}
};
render() {
const { groups, collection, auth } = this.props;
const { groups, collection, auth, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
return (
<Flex column>
<HelpText>
Cant find the group youre looking for?{" "}
{t("Cant find the group youre looking for?")}{" "}
<a role="button" onClick={this.handleNewGroupModalOpen}>
Create a group
{t("Create a group")}
</a>
.
</HelpText>
<Input
type="search"
placeholder="Search by group name…"
placeholder={t("Search by group name…")}
value={this.query}
onChange={this.handleFilter}
label="Search groups"
label={t("Search groups")}
labelHidden
flex
/>
<PaginatedList
empty={
this.query ? (
<Empty>No groups matching your search</Empty>
<Empty>{t("No groups matching your search")}</Empty>
) : (
<Empty>No groups left to add</Empty>
<Empty>{t("No groups left to add")}</Empty>
)
}
items={groups.notInCollection(collection.id, this.query)}
@@ -108,7 +115,7 @@ class AddGroupsToCollection extends React.Component<Props> {
renderActions={() => (
<ButtonWrap>
<Button onClick={() => this.handleAddGroup(item)} neutral>
Add
{t("Add")}
</Button>
</ButtonWrap>
)}
@@ -116,7 +123,7 @@ class AddGroupsToCollection extends React.Component<Props> {
)}
/>
<Modal
title="Create a group"
title={t("Create a group")}
onRequestClose={this.handleNewGroupModalClose}
isOpen={this.newGroupModalOpen}
>

View File

@@ -3,6 +3,7 @@ import { debounce } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation } from "react-i18next";
import AuthStore from "stores/AuthStore";
import MembershipsStore from "stores/MembershipsStore";
import UiStore from "stores/UiStore";
@@ -26,6 +27,7 @@ type Props = {
onSubmit: () => void,
};
@withTranslation()
@observer
class AddPeopleToCollection extends React.Component<Props> {
@observable inviteModalOpen: boolean = false;
@@ -51,39 +53,42 @@ class AddPeopleToCollection extends React.Component<Props> {
}, 250);
handleAddUser = (user) => {
const { t } = this.props;
try {
this.props.memberships.create({
collectionId: this.props.collection.id,
userId: user.id,
permission: "read_write",
});
this.props.ui.showToast(`${user.name} was added to the collection`);
this.props.ui.showToast(
t("{{ userName }} was added to the collection", { userName: user.name })
);
} catch (err) {
this.props.ui.showToast("Could not add user");
this.props.ui.showToast(t("Could not add user"));
}
};
render() {
const { users, collection, auth } = this.props;
const { users, collection, auth, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
return (
<Flex column>
<HelpText>
Need to add someone whos not yet on the team yet?{" "}
{t("Need to add someone whos not yet on the team yet?")}{" "}
<a role="button" onClick={this.handleInviteModalOpen}>
Invite people to {team.name}
{t("Invite people to {{ teamName }}", { teamName: team.name })}
</a>
.
</HelpText>
<Input
type="search"
placeholder="Search by name…"
placeholder={t("Search by name…")}
value={this.query}
onChange={this.handleFilter}
label="Search people"
label={t("Search people")}
autoFocus
labelHidden
flex
@@ -91,9 +96,9 @@ class AddPeopleToCollection extends React.Component<Props> {
<PaginatedList
empty={
this.query ? (
<Empty>No people matching your search</Empty>
<Empty>{t("No people matching your search")}</Empty>
) : (
<Empty>No people left to add</Empty>
<Empty>{t("No people left to add")}</Empty>
)
}
items={users.notInCollection(collection.id, this.query)}
@@ -108,7 +113,7 @@ class AddPeopleToCollection extends React.Component<Props> {
)}
/>
<Modal
title="Invite people"
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>

View File

@@ -1,4 +1,5 @@
// @flow
import i18n from "i18next";
import * as React from "react";
import styled from "styled-components";
import CollectionGroupMembership from "models/CollectionGroupMembership";
@@ -7,9 +8,11 @@ import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import GroupListItem from "components/GroupListItem";
import InputSelect from "components/InputSelect";
const t = (k) => i18n.t(k);
const PERMISSIONS = [
{ label: "Read only", value: "read" },
{ label: "Read & Edit", value: "read_write" },
{ label: t("Read only"), value: "read" },
{ label: t("Read & Edit"), value: "read_write" },
];
type Props = {
group: Group,
@@ -32,7 +35,7 @@ const MemberListItem = ({
renderActions={({ openMembersModal }) => (
<>
<Select
label="Permissions"
label={t("Permissions")}
options={PERMISSIONS}
value={
collectionGroupMembership
@@ -45,10 +48,12 @@ const MemberListItem = ({
<ButtonWrap>
<DropdownMenu>
<DropdownMenuItem onClick={openMembersModal}>
Members
{t("Members…")}
</DropdownMenuItem>
<hr />
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
<DropdownMenuItem onClick={onRemove}>
{t("Remove")}
</DropdownMenuItem>
</DropdownMenu>
</ButtonWrap>
</>

View File

@@ -1,4 +1,5 @@
// @flow
import i18n from "i18next";
import * as React from "react";
import styled from "styled-components";
import Membership from "models/Membership";
@@ -12,9 +13,11 @@ import InputSelect from "components/InputSelect";
import ListItem from "components/List/Item";
import Time from "components/Time";
const t = (k) => i18n.t(k);
const PERMISSIONS = [
{ label: "Read only", value: "read" },
{ label: "Read & Edit", value: "read_write" },
{ label: t("Read only"), value: "read" },
{ label: t("Read & Edit"), value: "read_write" },
];
type Props = {
user: User,
@@ -40,13 +43,15 @@ const MemberListItem = ({
<>
{user.lastActiveAt ? (
<>
Active <Time dateTime={user.lastActiveAt} /> ago
{t("Active {{ lastActiveAt }} ago", {
lastActiveAt: <Time dateTime={user.lastActiveAt} />,
})}
</>
) : (
"Never signed in"
t("Never signed in")
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
{!user.lastActiveAt && <Badge>{t("Invited")}</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</>
}
image={<Avatar src={user.avatarUrl} size={40} />}
@@ -54,7 +59,7 @@ const MemberListItem = ({
<Flex align="center">
{canEdit && onUpdate && (
<Select
label="Permissions"
label={t("Permissions")}
options={PERMISSIONS}
value={membership ? membership.permission : undefined}
onChange={(ev) => onUpdate(ev.target.value)}
@@ -64,12 +69,14 @@ const MemberListItem = ({
&nbsp;&nbsp;
{canEdit && onRemove && (
<DropdownMenu>
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
<DropdownMenuItem onClick={onRemove}>
{t("Remove")}
</DropdownMenuItem>
</DropdownMenu>
)}
{canEdit && onAdd && (
<Button onClick={onAdd} neutral>
Add
{t("Add")}
</Button>
)}
</Flex>

View File

@@ -1,4 +1,5 @@
// @flow
import i18n from "i18next";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import User from "models/User";
@@ -8,6 +9,8 @@ import Button from "components/Button";
import ListItem from "components/List/Item";
import Time from "components/Time";
const t = (k) => i18n.t(k);
type Props = {
user: User,
canEdit: boolean,
@@ -23,19 +26,21 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
<>
{user.lastActiveAt ? (
<>
Active <Time dateTime={user.lastActiveAt} /> ago
{t("Active {{ lastActiveAt }} ago", {
lastActiveAt: <Time dateTime={user.lastActiveAt} />,
})}
</>
) : (
"Never signed in"
t("Never signed in")
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
{!user.lastActiveAt && <Badge>{t("Invited")}</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</>
}
actions={
canEdit ? (
<Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral>
Add
{t("Add")}
</Button>
) : undefined
}

View File

@@ -125,6 +125,7 @@ class Profile extends React.Component<Props> {
options={[
{ label: "English (US)", value: "en_US" },
{ label: "Deutsch (Deutschland)", value: "de_DE" },
{ label: "Português (Portugal)", value: "pt_PT" },
]}
value={this.language}
onChange={this.handleLanguageChange}

View File

@@ -1,19 +1,143 @@
{
"Home": "Startseite",
"Recently updated": "Zuletzt geändert",
"Recently viewed": "Zuletzt gesehen",
"Created by me": "Von mir erstellt",
"Profile saved": "Profil gespeichert",
"Profile picture updated": "Profilfoto wurde aktualisiert",
"Unable to upload new profile picture": "Neuer Avatar kann nicht hochgeladen werden",
"Profile": "Profil",
"Photo": "Foto",
"Upload": "Hochladen",
"Full name": "Vorname",
"Language": "Sprache",
"Saving…": "Am speichern…",
"Save": "Speichern",
"Delete Account": "Konto löschen",
"You may delete your account at any time, note that this is unrecoverable": "Sie können Ihr Konto jederzeit löschen. Beachten Sie, dass dies nicht wiederherstellbar ist",
"Delete account": "Konto löschen"
}
"<strong>{{ userName }} {{ you }} <br /> {{ action }}</strong>": "<strong>{{ userName }} {{ you }} <br /> {{ action }}</strong>",
"Trash": "Papierkorb",
"Archive": "Archiv",
"Drafts": "Entwürfe",
"Templates": "Vorlagen",
"New": "Neu",
"Only visible to you": "Nur für Sie sichtbar",
"Draft": "Entwurf",
"Template": "Vorlag",
"New doc": "Neues Dok",
"why you not here": "warum sind sie nicht hier",
"More options": "Mehr Optionen",
"Search...": "Suche...",
"New collection…": "Neue Kollektion…",
"Collections": "Kollektionen",
"Untitled": "Ohne Titel",
"Home": "Startseite",
"Search": "Suche",
"Starred": "Favoriten",
"Invite people…": "Personen einladen…",
"Invite people": "Personen einalden",
"Create a collection": "Kollektion erstellen",
"Keyboard shortcuts": "Tastarturkürzel",
"Settings": "Einstellungen",
"API documentation": "API Dokumentation",
"Changelog": "Changelog",
"Send us feedback": "Schicken Sie uns Feedback",
"Report a bug": "Ein technischen Fehler melden",
"Appearance": "Darstellung",
"System": "System",
"Light": "Hell",
"Dark": "Dunkel",
"Log out": "Ausloggen",
"Collection permissions": "Kollektionsberechtigungen",
"New document": "Neues Dokument",
"Import document": "Dokument importieren",
"Edit…": "Bearbeiten…",
"Permissions…": "Berechtigungen…",
"Export…": "Export…",
"Delete…": "Löschen…",
"Edit collection": "Kollektion bearbeiten",
"Delete collection": "Kollektion löschen",
"Export collection": "Kollektion exportieren",
"Document duplicated": "Kollektion wurde dupliziert",
"Document archived": "Kollektion wurde archiviert",
"Document restored": "Kollektion wurde wiederhergestellt",
"Document unpublished": "Kollektion wurde unveröffentlicht",
"Restore": "Wiederherstellen",
"Restore…": "Wiederherstellen…",
"Choose a collection": "Kollektion auswählen",
"Unpin": "Unpin",
"Pin to collection": "Zur Kollektion anpinnen",
"Unstar": "Stern entfernen",
"Star": "Stern",
"Create a public share link": "Einen öffentlichen Freigabelink erstellen",
"Share link…": "Link teilen…",
"Enable embeds": "Embeds aktivieren",
"Disable embeds": "Embeds deaktivieren",
"Create a nested document inside the current document": "Erstellen Sie ein verschachteltes Dokument im aktuellen Dokument",
"New nested document": "Neues verschachteltes Dokument",
"Create template…": "Vorlage erstellen…",
"Unpublish": "Unveröffentlichen",
"Edit": "Bearbeiten",
"Duplicate": "Duplizieren",
"Move…": "Verschieben…",
"History": "Verlauf",
"Download": "Download",
"Print": "Drucken",
"Delete {{ documentName }}": "{{ documentName }} löschen",
"Create template": "Vorlage erstellen",
"Share document": "Dokument teilen",
"Edit group": "Gruppe bearbeiten",
"Delete group": "Gruppe löschen",
"Members…": "Mitglieder…",
"New document in": "Neues Dokument in der ",
"collection": "kollektion",
"New template…": "Neue Vorlage…",
"Link copied": "Link wurde kopiert",
"Restore version": "Version wiederherstellen",
"Copy link": "Link kopieren",
"Share link revoked": "Freigabelink wurde gelöscht",
"Share link copied": "Freigabelink wurde kopiert",
"Go to document": "Zum Dokument gehen",
"Revoke link": "Link löschen",
"By {{ author }}": "Von {{ author }}",
"Are you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Möchten Sie {{userName}} zum Administrator machen? Administratoren können Team- und Rechnungsinformationen ändern.",
"Are you want to make {{ userName }} a member?": "Möchten Sie {{userName}} zu einem Mitglied machen?",
"Are you want to suspend this account? Suspended users will be prevented from logging in.": "Möchten Sie dieses Konto sperren? Gesperrte Benutzer können sich nicht anmelden.",
"Make {{ userName }} a member…": "Machen Sie {{userName}} zu einem Mitglied…",
"Make {{ userName }} an admin…": "Machen Sie {{userName}} zu einem Administrator…",
"Revoke invite…": "Einladung löschen…",
"Activate account": "Konto aktivieren",
"Suspend account…": "Konto suspendieren…",
"Search in collection…": "Innerhalb der Kollektion suchen…",
"\n <strong>{{ collectionName }}</strong> doesnt contain any\n documents yet.\n <br />\n Get started by creating a new one!\n ": "\n <strong>{{ collectionName }}</strong> enthält noch keine\n Dokumente.\n <br />\n Erstellen Sie zunächst ein neues!\n ",
"Create a document": "Dokument erstellen",
"Manage members…": "Mitglieder verwalten…",
"Pinned": "Gepinned",
"Recently updated": "Vor Kurzem aktualisiert",
"Recently published": "Vor Kurzem veröffentlicht",
"Least recently updated": "Vor kurzem aktualisiert",
"AZ": "AZ",
"{{ groupName }} was added to the collection": "{{ groupName }} wurde zur Kollektion hinzugefügt",
"Could not add user": "Benutzer kann nicht hinzugefügt werden",
"Cant find the group youre looking for?": "Können Sie die Gruppe nicht finden?",
"Create a group": "Gruppe erstellen",
"Search by group name…": "Nach Name der Gruppe suchen…",
"Search groups": "Gruppen suchen",
"No groups matching your search": "Keine Gruppen, die Ihrer Suche entsprechen",
"No groups left to add": "Keine Gruppen mehr zum Hinzufügen",
"Add": "Hinzufügen",
"{{ userName }} was added to the collection": "{{ userName }} wurde zur Kollektion hinzugefügt",
"Need to add someone whos not yet on the team yet?": "Müssen Sie jemanden hinzufügen, der noch nicht im Team ist?",
"Invite people to {{ teamName }}": "Personen zu {{ teamName }} einladen",
"Search by name…": "Nach Name suchen…",
"Search people": "Personen suchen",
"No people matching your search": "Keine Personen, die Ihrer Suche entsprechen",
"No people left to add": "Keine Personen mehr zum Hinzufügen",
"Read only": "Schreibgeschützt",
"Read & Edit": "Lesen & Schreiben",
"Permissions": "Berechtigungen",
"Remove": "Entfernen",
"Active {{ lastActiveAt }} ago": "Vor {{ lastActiveAt }} aktiv",
"Never signed in": "Nie angemeldet",
"Invited": "Eingeladet",
"Admin": "Admin",
"Recently viewed": "Vor Kurzem angesehen",
"Created by me": "Von mir erstellt",
"Profile saved": "Profil gespeichert",
"Profile picture updated": "Profilbild wurde aktualisiert",
"Unable to upload new profile picture": "Neues Profilbild kann nicht hochgeladen werden",
"Profile": "Profil",
"Photo": "Foto",
"Upload": "Upload",
"Full name": "Vorname",
"Language": "Sprache",
"Saving…": "Am speichern…",
"Save": "Speichern",
"Delete Account": "Konto löschen",
"You may delete your account at any time, note that this is unrecoverable": "Sie können Ihr Konto jederzeit löschen. Beachten Sie, dass dies nicht wiederherstellbar ist",
"Delete account": "Konto löschen"
}

View File

@@ -1,6 +1,130 @@
{
"<strong>{{ userName }} {{ you }} <br /> {{ action }}</strong>": "<strong>{{ userName }} {{ you }} <br /> {{ action }}</strong>",
"Trash": "Trash",
"Archive": "Archive",
"Drafts": "Drafts",
"Templates": "Templates",
"New": "New",
"Only visible to you": "Only visible to you",
"Draft": "Draft",
"Template": "Template",
"New doc": "New doc",
"why you not here": "why you not here",
"More options": "More options",
"Search...": "Search...",
"New collection…": "New collection…",
"Collections": "Collections",
"Untitled": "Untitled",
"Home": "Home",
"Search": "Search",
"Starred": "Starred",
"Invite people…": "Invite people…",
"Invite people": "Invite people",
"Create a collection": "Create a collection",
"Keyboard shortcuts": "Keyboard shortcuts",
"Settings": "Settings",
"API documentation": "API documentation",
"Changelog": "Changelog",
"Send us feedback": "Send us feedback",
"Report a bug": "Report a bug",
"Appearance": "Appearance",
"System": "System",
"Light": "Light",
"Dark": "Dark",
"Log out": "Log out",
"Collection permissions": "Collection permissions",
"New document": "New document",
"Import document": "Import document",
"Edit…": "Edit…",
"Permissions…": "Permissions…",
"Export…": "Export…",
"Delete…": "Delete…",
"Edit collection": "Edit collection",
"Delete collection": "Delete collection",
"Export collection": "Export collection",
"Document duplicated": "Document duplicated",
"Document archived": "Document archived",
"Document restored": "Document restored",
"Document unpublished": "Document unpublished",
"Restore": "Restore",
"Restore…": "Restore…",
"Choose a collection": "Choose a collection",
"Unpin": "Unpin",
"Pin to collection": "Pin to collection",
"Unstar": "Unstar",
"Star": "Star",
"Create a public share link": "Create a public share link",
"Share link…": "Share link…",
"Enable embeds": "Enable embeds",
"Disable embeds": "Disable embeds",
"Create a nested document inside the current document": "Create a nested document inside the current document",
"New nested document": "New nested document",
"Create template…": "Create template…",
"Unpublish": "Unpublish",
"Edit": "Edit",
"Duplicate": "Duplicate",
"Move…": "Move…",
"History": "History",
"Download": "Download",
"Print": "Print",
"Delete {{ documentName }}": "Delete {{ documentName }}",
"Create template": "Create template",
"Share document": "Share document",
"Edit group": "Edit group",
"Delete group": "Delete group",
"Members…": "Members…",
"New document in": "New document in",
"collection": "collection",
"New template…": "New template…",
"Link copied": "Link copied",
"Restore version": "Restore version",
"Copy link": "Copy link",
"Share link revoked": "Share link revoked",
"Share link copied": "Share link copied",
"Go to document": "Go to document",
"Revoke link": "Revoke link",
"By {{ author }}": "By {{ author }}",
"Are you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Are you want to make {{ userName }} an admin? Admins can modify team and billing information.",
"Are you want to make {{ userName }} a member?": "Are you want to make {{ userName }} a member?",
"Are you want to suspend this account? Suspended users will be prevented from logging in.": "Are you want to suspend this account? Suspended users will be prevented from logging in.",
"Make {{ userName }} a member…": "Make {{ userName }} a member…",
"Make {{ userName }} an admin…": "Make {{ userName }} an admin…",
"Revoke invite…": "Revoke invite…",
"Activate account": "Activate account",
"Suspend account…": "Suspend account…",
"Search in collection…": "Search in collection…",
"\n <strong>{{ collectionName }}</strong> doesnt contain any\n documents yet.\n <br />\n Get started by creating a new one!\n ": "\n <strong>{{ collectionName }}</strong> doesnt contain any\n documents yet.\n <br />\n Get started by creating a new one!\n ",
"Create a document": "Create a document",
"Manage members…": "Manage members…",
"Pinned": "Pinned",
"Recently updated": "Recently updated",
"Recently published": "Recently published",
"Least recently updated": "Least recently updated",
"AZ": "AZ",
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
"Could not add user": "Could not add user",
"Cant find the group youre looking for?": "Cant find the group youre looking for?",
"Create a group": "Create a group",
"Search by group name…": "Search by group name…",
"Search groups": "Search groups",
"No groups matching your search": "No groups matching your search",
"No groups left to add": "No groups left to add",
"Add": "Add",
"{{ userName }} was added to the collection": "{{ userName }} was added to the collection",
"Need to add someone whos not yet on the team yet?": "Need to add someone whos not yet on the team yet?",
"Invite people to {{ teamName }}": "Invite people to {{ teamName }}",
"Search by name…": "Search by name…",
"Search people": "Search people",
"No people matching your search": "No people matching your search",
"No people left to add": "No people left to add",
"Read only": "Read only",
"Read & Edit": "Read & Edit",
"Permissions": "Permissions",
"Remove": "Remove",
"Active {{ lastActiveAt }} ago": "Active {{ lastActiveAt }} ago",
"Never signed in": "Never signed in",
"Invited": "Invited",
"Admin": "Admin",
"Recently viewed": "Recently viewed",
"Created by me": "Created by me",
"Profile saved": "Profile saved",

View File

@@ -2,8 +2,8 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en_US from "./default.json";
import de_DE from "./de_DE.json";
import en_US from "./default.json";
import pt_PT from "./pt_PT.json";
const outlineTranslation = {
@@ -15,7 +15,10 @@ const outlineTranslation = {
react: {
useSuspense: false,
},
lng: "DEFAULT_LANGUAGE" in process.env ? process.env.DEFAULT_LANGUAGE : "en_US",
lng:
"DEFAULT_LANGUAGE" in process.env
? process.env.DEFAULT_LANGUAGE
: "en_US",
debug: process.env.NODE_ENV !== "production",
resources: {
en_US: {
@@ -25,11 +28,11 @@ const outlineTranslation = {
translation: de_DE,
},
pt_PT: {
translation: pt_PT
}
translation: pt_PT,
},
},
});
}
}
},
};
export { outlineTranslation, i18n, en_US, de_DE, pt_PT };

View File

@@ -1,97 +1,97 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import { outlineTranslation, i18n, en_US, de_DE, pt_PT } from "./i18n";
import { isEqual } from "lodash";
import { outlineTranslation, i18n, en_US, de_DE, pt_PT } from "./i18n";
describe("i18n configuration", () => {
beforeEach(() => {
outlineTranslation.init();
});
it("all languages should have same keys", () => {
const en_US_Keys = Object.keys(en_US);
const de_DE_Keys = Object.keys(de_DE);
const pt_PT_Keys = Object.keys(pt_PT);
beforeEach(() => {
outlineTranslation.init();
});
it("all languages should have same keys", () => {
const en_US_Keys = Object.keys(en_US);
const de_DE_Keys = Object.keys(de_DE);
const pt_PT_Keys = Object.keys(pt_PT);
expect(isEqual(en_US_Keys, de_DE_Keys)).toBe(true);
expect(isEqual(en_US_Keys, pt_PT_Keys)).toBe(true);
});
expect(isEqual(en_US_Keys, de_DE_Keys)).toBe(true);
expect(isEqual(en_US_Keys, pt_PT_Keys)).toBe(true);
});
});
describe("i18n process.env is unset", () => {
beforeEach(() => {
delete process.env.DEFAULT_LANGUAGE;
outlineTranslation.init();
});
beforeEach(() => {
delete process.env.DEFAULT_LANGUAGE;
outlineTranslation.init();
});
it("translation of key should match", () => expect(i18n.t("Saving…")).toBe("Saving…"));
it("translation of key should match", () =>
expect(i18n.t("Saving…")).toBe("Saving…"));
it ("translation if changed to de_DE", () => {
i18n.changeLanguage("de_DE");
expect(i18n.t("Saving…")).toBe("Am speichern…");
});
it ("translation if changed to pt_PT", () => {
i18n.changeLanguage("pt_PT");
expect(i18n.t("Saving…")).toBe("A guardar…");
});
it("translation if changed to de_DE", () => {
i18n.changeLanguage("de_DE");
expect(i18n.t("Saving…")).toBe("Am speichern…");
});
it("translation if changed to pt_PT", () => {
i18n.changeLanguage("pt_PT");
expect(i18n.t("Saving…")).toBe("A guardar…");
});
});
describe("i18n process.env is en_US", () => {
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "en_US";
outlineTranslation.init();
});
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "en_US";
outlineTranslation.init();
});
it("translation of key should match", () => expect(i18n.t("Saving…")).toBe("Saving…"));
it("translation of key should match", () =>
expect(i18n.t("Saving…")).toBe("Saving…"));
it ("translation if changed to de_DE", () => {
i18n.changeLanguage("de_DE");
expect(i18n.t("Saving…")).toBe("Am speichern…");
});
it ("translation if changed to pt_PT", () => {
i18n.changeLanguage("pt_PT");
expect(i18n.t("Saving…")).toBe("A guardar…");
});
it("translation if changed to de_DE", () => {
i18n.changeLanguage("de_DE");
expect(i18n.t("Saving…")).toBe("Am speichern…");
});
it("translation if changed to pt_PT", () => {
i18n.changeLanguage("pt_PT");
expect(i18n.t("Saving…")).toBe("A guardar…");
});
});
describe("i18n process.env is de_DE", () => {
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "de_DE";
outlineTranslation.init();
});
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "de_DE";
outlineTranslation.init();
});
it("translation of key should match", () => expect(i18n.t("Saving…")).toBe("Am speichern…"));
it("translation of key should match", () =>
expect(i18n.t("Saving…")).toBe("Am speichern…"));
it ("translation if changed to en_US", () => {
i18n.changeLanguage("en_US");
expect(i18n.t("Saving…")).toBe("Saving…");
});
it ("translation if changed to pt_PT", () => {
i18n.changeLanguage("pt_PT");
expect(i18n.t("Saving…")).toBe("A guardar…");
});
it("translation if changed to en_US", () => {
i18n.changeLanguage("en_US");
expect(i18n.t("Saving…")).toBe("Saving…");
});
it("translation if changed to pt_PT", () => {
i18n.changeLanguage("pt_PT");
expect(i18n.t("Saving…")).toBe("A guardar…");
});
});
describe("i18n process.env is pt_PT", () => {
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "pt_PT";
outlineTranslation.init();
});
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "pt_PT";
outlineTranslation.init();
});
it("translation of key should match", () => expect(i18n.t("Saving…")).toBe("A guardar…"));
it("translation of key should match", () =>
expect(i18n.t("Saving…")).toBe("A guardar…"));
it ("translation if changed to en_US", () => {
i18n.changeLanguage("en_US");
expect(i18n.t("Saving…")).toBe("Saving…");
});
it("translation if changed to en_US", () => {
i18n.changeLanguage("en_US");
expect(i18n.t("Saving…")).toBe("Saving…");
});
it ("translation if changed to de_DE", () => {
i18n.changeLanguage("de_DE");
expect(i18n.t("Saving…")).toBe("Am speichern…");
});
});
it("translation if changed to de_DE", () => {
i18n.changeLanguage("de_DE");
expect(i18n.t("Saving…")).toBe("Am speichern…");
});
});

View File

@@ -1,20 +1,143 @@
{
"Home": "Home",
"Recently updated": "Recently updated",
"Recently viewed": "Recently viewed",
"Created by me": "Created by me",
"Profile saved": "Profile saved",
"Profile picture updated": "Profile picture updated",
"Unable to upload new profile picture": "Unable to upload new avatar",
"Profile": "Profile",
"Photo": "Photo",
"Upload": "Upload",
"Full name": "Full name",
"Language": "Language",
"Saving…": "A guardar…",
"Save": "Save",
"Delete Account": "Delete Account",
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
"Delete account": "Delete account"
}
"<strong>{{ userName }} {{ you }} <br /> {{ action }}</strong>": "<strong>{{ userName }} {{ you }} <br /> {{ action }}</strong>",
"Trash": "Reciclagem",
"Archive": "Arquivo",
"Drafts": "Rascunhos",
"Templates": "Templates",
"New": "Novo",
"Only visible to you": "Apenas visível para ti",
"Draft": "Rascunho",
"Template": "Template",
"New doc": "Novo doc",
"why you not here": "porquê é que não estás aquí",
"More options": "Mais opções",
"Search...": "Pesquisa...",
"New collection…": "Nova coleção…",
"Collections": "Coleções",
"Untitled": "Sem título",
"Home": "Página inicial",
"Search": "Pesquisa",
"Starred": "Favoritos",
"Invite people…": "Convidar pessoas…",
"Invite people": "Convidar pessoas",
"Create a collection": "Criar coleção",
"Keyboard shortcuts": "Atalhos do teclado",
"Settings": "Configurações",
"API documentation": "Documentação da API",
"Changelog": "Changelog",
"Send us feedback": "Envia-nos Feedback",
"Report a bug": "Reportar um erro técnico",
"Appearance": "Aparência",
"System": "Sistema",
"Light": "Claro",
"Dark": "Escuro",
"Log out": "Deslogar",
"Collection permissions": "Permissões da coleção",
"New document": "Novo documento",
"Import document": "Importar documento",
"Edit…": "Editar…",
"Permissions…": "Permissões…",
"Export…": "Exportar…",
"Delete…": "Apagar…",
"Edit collection": "Editar coleção",
"Delete collection": "Apagar coleção",
"Export collection": "Exportar coleção",
"Document duplicated": "Documento duplicado",
"Document archived": "Documento arquivado",
"Document restored": "Documento restaurado",
"Document unpublished": "Documento não publicado",
"Restore": "Restaurar",
"Restore…": "Restaurar…",
"Choose a collection": "Escolher coleção",
"Unpin": "Tirar pino",
"Pin to collection": "Pin coleção",
"Unstar": "Tirar estrela",
"Star": "Estrela",
"Create a public share link": "Criar um link de partilha público",
"Share link…": "Partilhar link…",
"Enable embeds": "Ativar embeds",
"Disable embeds": "Desativar embeds",
"Create a nested document inside the current document": "Criar um documento aninhado dentro do documento atual",
"New nested document": "Novo documento aninhado",
"Create template…": "Criar template…",
"Unpublish": "Despublicar",
"Edit": "Editar",
"Duplicate": "Duplicar",
"Move…": "Mover…",
"History": "Historia",
"Download": "Download",
"Print": "Imprimir",
"Delete {{ documentName }}": "Apagar {{ documentName }}",
"Create template": "Criar template",
"Share document": "Partilhar documento",
"Edit group": "Editar grupo",
"Delete group": "Apagar grupo",
"Members…": "Membros…",
"New document in": "Novo documento em",
"collection": "coleção",
"New template…": "Novo template…",
"Link copied": "Link copiado",
"Restore version": "Restaurar versão",
"Copy link": "Copiar link",
"Share link revoked": "Link de partilha foi revogado",
"Share link copied": "Link de partilha foi copiado",
"Go to document": "Ir para documento",
"Revoke link": "Revogar link",
"By {{ author }}": "De {{ author }}",
"Are you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Deseja tornar {{userName}} um administrador? Os administradores podem modificar as informações da equipe e do faturamento.",
"Are you want to make {{ userName }} a member?": "Deseja tornar {{userName}} um membro?",
"Are you want to suspend this account? Suspended users will be prevented from logging in.": "Deseja suspender esta conta? Os usuários suspensos serão impedidos de fazer login.",
"Make {{ userName }} a member…": "Tornar {{userName}} um membro…",
"Make {{ userName }} an admin…": "Tornar {{userName}} um administrador…",
"Revoke invite…": "Revogar convite…",
"Activate account": "Ativar conta",
"Suspend account…": "Suspender conta…",
"Search in collection…": "Procurar na coleção…",
"\n <strong>{{ collectionName }}</strong> doesnt contain any\n documents yet.\n <br />\n Get started by creating a new one!\n ": "\n <strong>{{ collectionName }}</strong> não contém nenhum documento\n ainda.\n <br />\n Para começar crie um documento novo!\n ",
"Create a document": "Criar documento",
"Manage members…": "Gerenciar membros…",
"Pinned": "Marcado",
"Recently updated": "Atualizado recentemente",
"Recently published": "Publicado recentemente",
"Least recently updated": "Menos atualizado recentemente",
"AZ": "AZ",
"{{ groupName }} was added to the collection": "{{ groupName }} foi adicionado à coleção",
"Could not add user": "Não foi possível adicionar usuário",
"Cant find the group youre looking for?": "Não consegue encontrar o grupo que procura?",
"Create a group": "Criar grupo",
"Search by group name…": "Procurar grupo pelo nome…",
"Search groups": "Procurar grupos",
"No groups matching your search": "Nenhum grupo corresponde à sua pesquisa",
"No groups left to add": "Nenhum grupo restante para adicionar",
"Add": "Adicionar",
"{{ userName }} was added to the collection": "{{userName}} foi adicionado à coleção",
"Need to add someone whos not yet on the team yet?": "Precisa adicionar alguém que ainda não faz parte da equipe?",
"Invite people to {{ teamName }}": "Convide pessoas para {{teamName}}",
"Search by name…": "Procurar pelo nome…",
"Search people": "Procurar pessoas",
"No people matching your search": "Nenhuma pessoa corresponde à sua pesquisa",
"No people left to add": "Não sobrou ninguém para adicionar",
"Read only": "Somente leitura",
"Read & Edit": "Leitura & Escrita",
"Permissions": "Permissões",
"Remove": "Remover",
"Active {{ lastActiveAt }} ago": "Ativo há {{ lastActiveAt }}",
"Never signed in": "Nunca conectado",
"Invited": "Convidado",
"Admin": "Admin",
"Recently viewed": "Visto recentemente",
"Created by me": "Criado por mim",
"Profile saved": "Profil foi guardado",
"Profile picture updated": "Foto de perfil foi atualizada",
"Unable to upload new profile picture": "Não é possível carregar uma nova foto de perfil",
"Profile": "Profil",
"Photo": "Foto",
"Upload": "Upload",
"Full name": "Nome completo",
"Language": "Língua",
"Saving…": "A guardar…",
"Save": "Guardar",
"Delete Account": "Apagar conta",
"You may delete your account at any time, note that this is unrecoverable": "Pode apagar a sua conta a qualquer momento, atenção que isso é irrecuperável",
"Delete account": "Apagar conta"
}