Compare commits

..

19 Commits

Author SHA1 Message Date
Henry
f797af64e7 feat: update comments to use tiptap editor 2026-02-08 22:35:42 +00:00
Henry
1f0c780763 feat: skip pending members in sendMentionEmails 2026-02-08 22:15:26 +00:00
Henry
149277ef39 fix: update the editor suggestion to show all members 2026-02-08 22:12:23 +00:00
Henry
c8dcb17510 feat: update card router to send emails on mention 2026-02-07 22:04:31 +00:00
Henry
2f4aea52eb feat: add repo funcs 2026-02-07 22:00:55 +00:00
Henry
7973d8de2f feat: add sendMentionEmail func 2026-02-07 21:58:52 +00:00
Henry
7a0f89c4c0 feat: add mention email template 2026-02-07 21:42:24 +00:00
Henry
105ba45412 chore: gen migration 2026-02-07 21:41:29 +00:00
Henry
3c46d82b7a feat: add notifications schema 2026-02-07 21:41:13 +00:00
Ivan Shelepugin
24e283e22c feat: smart typography (#360)
* build(web, deps): add `@tiptap/extension-typography`

* feat(editor): add typography extension

* chore(editor): disable some typographic characters

* chore(editor): disable superscript
2026-02-07 20:04:37 +00:00
Matt
07db7409cb fix: remove phantom spacing below social providers in auth form (closes #334) (#364) 2026-02-07 19:34:06 +00:00
Charity
346ddc16be feat: add functionality to copy card link (#358)
* Implemented link copying feature in CardDropdown and CardModal components.
* Updated Dropdown component to accept new props for card identification.
2026-02-06 23:34:23 +00:00
Charity
f9eee53ed0 fix: board and workspace URL links and copy behavior (#357)
- Use /boards/{id} for private board links so copied link works; keep pretty URL for public boards
- Show matching path in board header (pretty path for public, boards/id for private)
- Copy link button copies URL to clipboard instead of opening in new tab (opening a new tab seems redundant since we're already on it)
- Edit workspace URL button links to /settings/workspace instead of account settings
2026-02-06 23:31:08 +00:00
Henry
1ab4e24c96 docs: add railway deployment template to self hosting docs (#363) 2026-02-05 21:52:40 +00:00
Henry
34165b390f fix: handle missing board in board router 2026-02-04 22:56:17 +00:00
Henry
ef0d53db8f refactor: standardise S3 URL generation (#362)
* refactor: replace presigned URL uploads with backend upload endpoints

* feat: update avatar upload to use new endpoint

* refactor: use createS3Client in auth hooks

* feat: generate presigned URLs for avatars

* fix: show avatar image in user menu

* fix: hide tooltip if content is empty

* fix: support external avatar URLs in generateAvatarUrl

* fix: remove content type restriction on attachments
2026-02-04 22:40:14 +00:00
Henry
78b9de869f fix: prevent validation errors in workspace/card queries (#356)
* fix: prevent workspace.byId validation errors when publicId is empty

* fix: add keys to shortcut elements

* fix: prevent card.byId validation errors when cardPublicId is empty

* chore: remove invalid config option from next.config.js
2026-02-02 22:25:54 +00:00
Henry
81c03b51e2 fix: fallback to email when user name is missing in activity list (#355)
* fix: fallback to email when user name is missing in activity list

* chore: gen translations
2026-02-02 22:00:03 +00:00
Henry
0d3d9a358f fix: prevent ellipses from showing through member dropdown menu (#352) 2026-02-02 20:05:53 +00:00
75 changed files with 6154 additions and 1054 deletions

View File

@@ -47,7 +47,17 @@ See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features.
## Self Hosting 🐳
The easiest way to self-host Kan is with Docker Compose. This will set up everything for you including your postgres database.
### One-click Deployments
The easiest way to deploy Kan is through Railway. We've partnered with Railway to maintain an official template that supports the development of the project.
<a href="https://railway.com/deploy/kan?referralCode=bZPsr2&utm_medium=integration&utm_source=template&utm_campaign=generic">
<img src="https://railway.app/button.svg" alt="Deploy on Railway" height="40" />
</a>
### Docker Compose
Alternatively, you can self-host Kan with Docker Compose. This will set up everything for you including your postgres database.
1. Create a new file called `docker-compose.yml` and paste the following configuration:

View File

@@ -5,6 +5,8 @@ checksums:
"%2C%20or%20see%20our/singular": e8a8f2a078fcaea03481710dcaccce5d
"%7B0%7D/singular": 18782768c8a0b3c2078747b34fbbcd24
"%7B0%7D%20%7C%20%7B1%7D/singular": 4628f9c6ec13c2dc2984a6ab80e19f8b
"%7B0%7D%20has%20been%20added%20to%20your%20favorites./singular": 0f752180183c80703b3f9f39dc97195f
"%7B0%7D%20has%20been%20removed%20from%20your%20favorites./singular": 26e4b4997ad8a711d0b3e46337799454
"%7B0%7D%20labels/singular": 785f92bedf786ff3e285adfc07c69d18
"%7B0%7D%20not%20found/singular": a2665fc66f128fee1a80ffec61c9f1a1
"%7BboardCount%2C%20plural%2C%20one%20%7BImport%20board%20(1)%7D%20other%20%7BImport%20boards%20(%7BboardCount%7D)%7D%7D/singular": 72358c550bd1a99fcb6e8952f0f947ef
@@ -27,6 +29,7 @@ checksums:
Add%20details.../singular: 2f42547fd5d199f173aa7a88c8a9b19a
Add%20label/singular: 0be732d46df263265935fda342097a08
Add%20member/singular: 11979625770516ca287e929381778e02
Add%20to%20favorites/singular: 532155acb0a46f2b6c37e15afb3ad21e
added%20%7B0%7D%20labels%3A%20%3C0%3E%7BlabelList%7D%3C%2F0%3E/singular: 42b0488d570626dc887c58ff669c953c
added%20a%20checklist/singular: 44304f4a5ef3a46378eea243b5d2df79
added%20a%20checklist%20item/singular: 20bc0330dba5a9ec978e04bec174365a
@@ -35,6 +38,7 @@ checksums:
added%20checklist%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: fb6a852c50c1629f47c6f155fc93fdee
added%20checklist%20item%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: bdd202da20b1fffbec21792c5453f90c
added%20label%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: b32be052b3d57de0c9120fa7f9fc86ee
Added%20to%20favorites/singular: 848046ec65d15913112b466c544722eb
Adding%20a%20new%20member%20will%20cost%20an%20additional%20%7Bprice%7D%20(%7BbillingType%7D)%20per%20seat./singular: 12e88573028306110fbc15ef1e714892
Adjust%20the%20square%20crop%20to%20fit%20your%20avatar./singular: a4df26bbce6f14c6962fac1324db00a8
Admin/singular: 90eb20f1400db82ab874744e47836dc6
@@ -106,7 +110,6 @@ checksums:
Can%20edit%20member%20roles%20and%20permissions/singular: c364e8e8866111a471f4081db0730049
Can%20edit%20workspace/singular: 9768f990844e215c06910fed250da23a
Can%20invite%20members/singular: e02e562bcb7f46008d9595e485951413
can%20manage%20workspace%20settings/singular: 78bfe1746f961f37b1cde85e5a0e9a13
Can%20manage%20workspace%20settings/singular: 27eb18d3be5b3d813996b7d5071e0317
Can%20remove%20members/singular: 97a452b0fb4b661eaca7e5b3d5b49dcd
Can%20view%20boards/singular: 14977bbbb566f72fc74e17d99bad09bb
@@ -462,10 +465,12 @@ checksums:
Recruitment/singular: 78084ee4954aa22efe9ad7463d2a0c72
Remote/singular: dc3e4280dfe5c455b38ba6c8884999cf
Remove/singular: dba2fe5fe9f83f8078c687f28cba4b52
Remove%20from%20favorites/singular: 74d9cc2d2f3c7a7dc42a7e62c8ee06d5
Remove%20member/singular: 1d77c2ca3768e486fd1fb5df30cb60d9
removed%20%7B0%7D%20labels%3A%20%3C0%3E%7BlabelList%7D%3C%2F0%3E/singular: 612fcfd4aca6818fc870f81d9687d63f
removed%20a%20label%20from%20the%20card/singular: f83ec18850a2a8a3f8242a740a04df06
removed%20a%20member%20from%20the%20card/singular: 0bd7561bb79358fde3d595c3e60a315a
Removed%20from%20favorites/singular: 540609ab686ed67b1cfd7649467b8bc1
removed%20label%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: 7e0936b15831e65754588b8512f62fcb
removed%20the%20due%20date/singular: 000c084844718540fcad542dd8caf8b4
renamed%20a%20checklist/singular: 4d208de1857740e63469007c5b8b491a
@@ -581,6 +586,7 @@ checksums:
Unable%20to%20reorder%20checklist%20item/singular: dcc238c72b85daf74ebd46adcd914473
Unable%20to%20reset%20permissions/singular: fb09d88ff4eb32afb852d733bafd515b
Unable%20to%20send%20feedback/singular: 656c93265d7e2bef1245b17d85f85ae5
Unable%20to%20update%20board/singular: 62899897d7bffcb1df3af77d460f0881
Unable%20to%20update%20board%20URL/singular: 080746884059142358b58d9a44ff7d93
Unable%20to%20update%20board%20visibility/singular: a76a21d561b8943e9276e027a1e3f70d
Unable%20to%20update%20card/singular: eca6002e57af8ec97324eae64e3b5731

View File

@@ -53,7 +53,6 @@ const config = {
{
protocol: 'https',
hostname: 'cdn.discordapp.com',
pathname: '/avatars/**',
},
];
@@ -95,12 +94,6 @@ const config = {
swcPlugins: [["@lingui/swc-plugin", {}]],
},
api: {
bodyParser: {
sizeLimit: env("NEXT_API_BODY_SIZE_LIMIT") || '1mb',
},
},
async rewrites() {
return [
{

View File

@@ -38,6 +38,7 @@
"@tiptap/extension-link": "^2.22.2",
"@tiptap/extension-mention": "^3.0.9",
"@tiptap/extension-placeholder": "^2.14.0",
"@tiptap/extension-typography": "^3.18.0",
"@tiptap/pm": "^2.14.0",
"@tiptap/react": "^2.14.0",
"@tiptap/starter-kit": "^2.14.0",

View File

@@ -361,8 +361,8 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
})}
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
{!isCredentialsEnabled && socialProviders?.length === 0 && (
{!(isCredentialsEnabled || isMagicLinkAvailable) &&
socialProviders?.length === 0 && (
<div className="flex w-full items-center gap-4">
<div className="h-[1px] w-1/3 bg-light-600 dark:bg-dark-600" />
<span className="text-center text-sm text-light-900 dark:text-dark-900">
@@ -371,65 +371,62 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
<div className="h-[1px] w-1/3 bg-light-600 dark:bg-dark-600" />
</div>
)}
{!isCredentialsEnabled && socialProviders?.length !== 0 && (
<div className="mb-[1.5rem] flex w-full items-center gap-4">
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
<span className="text-sm text-light-900 dark:text-dark-900">
{t`or`}
</span>
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
</div>
)}
<div className="space-y-2">
{isSignUp && isCredentialsEnabled && (
<div>
<Input
{...register("name", { required: true })}
placeholder={t`Enter your name`}
/>
{errors.name && (
<p className="mt-2 text-xs text-red-400">
{t`Please enter a valid name`}
</p>
)}
{(isCredentialsEnabled || isMagicLinkAvailable) && (
<form onSubmit={handleSubmit(onSubmit)}>
{socialProviders?.length !== 0 && (
<div className="mb-[1.5rem] flex w-full items-center gap-4">
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
<span className="text-sm text-light-900 dark:text-dark-900">
{t`or`}
</span>
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
</div>
)}
{(isCredentialsEnabled || isMagicLinkAvailable) && (
<>
<div className="space-y-2">
{isSignUp && isCredentialsEnabled && (
<div>
<Input
{...register("email", { required: true })}
placeholder={t`Enter your email address`}
{...register("name", { required: true })}
placeholder={t`Enter your name`}
/>
{errors.email && (
{errors.name && (
<p className="mt-2 text-xs text-red-400">
{t`Please enter a valid email address`}
{t`Please enter a valid name`}
</p>
)}
</div>
{isCredentialsEnabled && (
<div>
<Input
type="password"
{...register("password", { required: true })}
placeholder={t`Enter your password`}
/>
{errors.password && (
<p className="mt-2 text-xs text-red-400">
{errors.password.message ??
t`Please enter a valid password`}
</p>
)}
</div>
)}
<div>
<Input
{...register("email", { required: true })}
placeholder={t`Enter your email address`}
/>
{errors.email && (
<p className="mt-2 text-xs text-red-400">
{t`Please enter a valid email address`}
</p>
)}
</>
)}
{loginError && (
<p className="mt-2 text-xs text-red-400">{loginError}</p>
)}
</div>
{(isCredentialsEnabled || isMagicLinkAvailable) && (
</div>
{isCredentialsEnabled && (
<div>
<Input
type="password"
{...register("password", { required: true })}
placeholder={t`Enter your password`}
/>
{errors.password && (
<p className="mt-2 text-xs text-red-400">
{errors.password.message ??
t`Please enter a valid password`}
</p>
)}
</div>
)}
{loginError && (
<p className="mt-2 text-xs text-red-400">{loginError}</p>
)}
</div>
<div className="mt-[1.5rem] flex items-center gap-4">
<Button
isLoading={isLoginWithEmailPending}
@@ -441,8 +438,11 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
{isMagicLinkMode ? t`magic link` : t`email`}
</Button>
</div>
)}
</form>
</form>
)}
{!(isCredentialsEnabled || isMagicLinkAvailable) && loginError && (
<p className="mt-2 text-xs text-red-400">{loginError}</p>
)}
</div>
);
}

View File

@@ -25,7 +25,7 @@ const Avatar = ({
icon?: React.ReactNode;
isLoading?: boolean;
}) => {
const initials = name
const initials = name?.trim()
? getInitialsFromName(name)
: inferInitialsFromEmail(email);

View File

@@ -12,6 +12,7 @@ import { authClient } from "@kan/auth/client";
import { useClickOutside } from "~/hooks/useClickOutside";
import { useModal } from "~/providers/modal";
import { useWorkspace, WorkspaceProvider } from "~/providers/workspace";
import { api } from "~/utils/api";
import SideNavigation from "./SideNavigation";
interface DashboardProps {
@@ -44,6 +45,12 @@ export default function Dashboard({
const { availableWorkspaces, hasLoaded } = useWorkspace();
const { data: session, isPending: sessionLoading } = authClient.useSession();
const { data: user, isLoading: userLoading } = api.user.getUser.useQuery(
undefined,
{
enabled: !!session?.user,
},
);
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
const [isRightPanelOpen, setIsRightPanelOpen] = useState(false);
@@ -155,8 +162,12 @@ export default function Dashboard({
className={`fixed top-12 z-40 h-[calc(100dvh-3rem)] w-[calc(100vw-1.5rem)] transform transition-transform duration-300 ease-in-out md:relative md:top-0 md:h-full md:w-auto md:translate-x-0 ${isSideNavOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"} `}
>
<SideNavigation
user={{ displayName: session?.user.name, email: session?.user.email, image: session?.user.image }}
isLoading={sessionLoading}
user={{
displayName: user?.name ?? session?.user.name,
email: user?.email ?? session?.user.email ?? "",
image: user?.image ?? undefined,
}}
isLoading={sessionLoading || userLoading}
onCloseSideNav={closeSideNav}
/>
</div>

View File

@@ -17,6 +17,7 @@ import {
ReactRenderer,
useEditor,
} from "@tiptap/react";
import Typography from "@tiptap/extension-typography";
import StarterKit from "@tiptap/starter-kit";
import Suggestion from "@tiptap/suggestion";
import {
@@ -386,46 +387,54 @@ export interface SlashNodeAttrs {
label?: string | null;
}
const CommandItems: SlashCommandItem[] = [
{
title: "Heading 1",
icon: <HiH1 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 1 }).run(),
},
{
title: "Heading 2",
icon: <HiH2 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 2 }).run(),
},
{
title: "Heading 3",
icon: <HiH3 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 3 }).run(),
},
{
title: "Bullet List",
icon: <HiOutlineListBullet />,
command: ({ editor }) => editor.chain().focus().toggleBulletList().run(),
},
{
title: "Ordered List",
icon: <HiOutlineNumberedList />,
command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(),
},
{
title: "Blockquote",
icon: <HiOutlineChatBubbleLeftEllipsis />,
command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(),
},
{
title: "Code Block",
icon: <HiOutlineCodeBracketSquare />,
command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(),
},
];
const getCommandItems = (disableHeadings: boolean): SlashCommandItem[] => {
const headingCommands: SlashCommandItem[] = disableHeadings
? []
: [
{
title: "Heading 1",
icon: <HiH1 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 1 }).run(),
},
{
title: "Heading 2",
icon: <HiH2 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 2 }).run(),
},
{
title: "Heading 3",
icon: <HiH3 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 3 }).run(),
},
];
return [
...headingCommands,
{
title: "Bullet List",
icon: <HiOutlineListBullet />,
command: ({ editor }) => editor.chain().focus().toggleBulletList().run(),
},
{
title: "Ordered List",
icon: <HiOutlineNumberedList />,
command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(),
},
{
title: "Blockquote",
icon: <HiOutlineChatBubbleLeftEllipsis />,
command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(),
},
{
title: "Code Block",
icon: <HiOutlineCodeBracketSquare />,
command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(),
},
];
};
export default function Editor({
content,
@@ -434,6 +443,8 @@ export default function Editor({
readOnly = false,
workspaceMembers,
enableYouTubeEmbed = true,
placeholder,
disableHeadings = false,
}: {
content: string | null;
onChange?: (value: string) => void;
@@ -441,18 +452,23 @@ export default function Editor({
readOnly?: boolean;
workspaceMembers: WorkspaceMember[];
enableYouTubeEmbed?: boolean;
placeholder?: string;
disableHeadings?: boolean;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const editor = useEditor(
{
extensions: [
StarterKit,
StarterKit.configure({
heading: disableHeadings ? false : undefined,
}),
Markdown,
Placeholder.configure({
placeholder: readOnly
? ""
: t`Add description... (type '/' to open commands or '@' to mention)`,
: placeholder ??
t`Add description... (type '/' to open commands or '@' to mention)`,
}),
Link.configure({
openOnClick: true,
@@ -465,10 +481,10 @@ export default function Editor({
autolink: true,
}),
SlashCommands.configure({
commandItems: CommandItems,
commandItems: getCommandItems(disableHeadings),
suggestion: {
items: ({ query }: { query: string }) =>
filterSlashCommandItems(CommandItems, query),
filterSlashCommandItems(getCommandItems(disableHeadings), query),
startOfLine: true,
char: "/",
},
@@ -480,20 +496,28 @@ export default function Editor({
suggestion: {
char: "@",
items: ({ query }: { query: string }) => {
const all: MentionItem[] = workspaceMembers.map(
(member: WorkspaceMember) => ({
id: member.publicId,
label: member?.user?.name ?? member.email,
image: member?.user?.image ?? null,
}),
const withEmail = workspaceMembers.filter((member) => member.email);
const mapped = withEmail.map((member: WorkspaceMember) => ({
id: member.publicId,
label: member?.user?.name?.trim() || member.email || "",
image: member?.user?.image ?? null,
}));
const all: MentionItem[] = mapped.filter(
(item) => item.label && item.label.length > 0,
);
const q = query.toLowerCase();
return all.filter(
(u) =>
u.label &&
typeof u.label === "string" &&
u.label.toLowerCase().includes(q),
const q = query.toLowerCase().trim();
if (q === "") {
return all;
}
const filtered = all.filter((u) =>
u.label.toLowerCase().includes(q),
);
return filtered;
},
command: ({ editor, range, props }) => {
const id = props.id ?? "";
@@ -514,6 +538,17 @@ export default function Editor({
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
},
}),
Typography.configure({
openDoubleQuote: false,
closeDoubleQuote: false,
openSingleQuote: false,
closeSingleQuote: false,
oneHalf: false,
oneQuarter: false,
threeQuarters: false,
superscriptTwo: false,
superscriptThree: false,
}),
...(enableYouTubeEmbed ? [YouTubeNode] : []),
],
content,

View File

@@ -55,9 +55,10 @@ export default function SideNavigation({
const [isInitialised, setIsInitialised] = useState(false);
const { openModal } = useModal();
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
const { data: workspaceData } = api.workspace.byId.useQuery(
{ workspacePublicId: workspace.publicId },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const subscriptions = workspaceData?.subscriptions as
| Subscription[]

View File

@@ -7,7 +7,7 @@ import tippy from "tippy.js";
interface TooltipProps {
children: ReactNode;
content: ReactNode;
content?: ReactNode;
placement?: Placement;
delay?: number | [number, number];
}
@@ -24,6 +24,8 @@ export function Tooltip({
useEffect(() => {
if (!triggerRef.current) return;
if (!content) return;
const container = document.createElement("div");
const root = createRoot(container);
rootRef.current = root;

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} wurde zu Ihren Favoriten hinzugefügt."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} wurde aus Ihren Favoriten entfernt."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Label hinzufügen"
msgid "Add member"
msgstr "Mitglied hinzufügen"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Zu Favoriten hinzufügen"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "{0} Labels hinzugefügt: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "hat eine Checkliste hinzugefügt"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "hat ein Checklistenelement hinzugefügt"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "hat ein Label zur Karte hinzugefügt"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "hat ein Mitglied zur Karte hinzugefügt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> hinzugefügt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> hinzugefügt"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "hat Label <0>{0}</0> hinzugefügt"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Zu Favoriten hinzugefügt"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Das Hinzufügen eines neuen Mitglieds kostet zusätzlich {price} ({billingType}) pro Platz."
@@ -282,8 +300,8 @@ msgstr "Sind Sie sicher, dass Sie dieses Label löschen möchten?"
msgid "Are you sure you want to delete your account?"
msgstr "Bist du sicher, dass du dein Konto löschen möchtest?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "hat <0>{0}</0> der Karte zugewiesen"
@@ -562,8 +580,8 @@ msgstr "Passwort ändern"
msgid "Change your language preferences."
msgstr "Ändern Sie Ihre Spracheinstellungen."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "hat den Fälligkeitstermin auf <0>{formattedDate}</0> geändert"
@@ -651,12 +669,12 @@ msgstr "Abgeschlossen"
msgid "Complete control and ownership:"
msgstr "Vollständige Kontrolle und Eigentum:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "hat ein Checklistenelement abgeschlossen"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> abgeschlossen"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Liste erstellen"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Neue(n) {0} erstellen"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Neues Label erstellen"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Neue Liste erstellen"
@@ -783,7 +801,7 @@ msgstr "Vorlage erstellen"
msgid "Create workspace"
msgstr "Arbeitsbereich erstellen"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "hat die Karte erstellt"
@@ -857,7 +875,7 @@ msgstr "Löschen"
msgid "Delete account"
msgstr "Konto löschen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Board löschen"
@@ -873,7 +891,7 @@ msgstr "Kommentar löschen"
msgid "Delete list"
msgstr "Liste löschen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Vorlage löschen"
@@ -883,21 +901,21 @@ msgstr "Vorlage löschen"
msgid "Delete workspace"
msgstr "Workspace löschen"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "hat eine Checkliste gelöscht"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "hat ein Checklistenelement gelöscht"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> gelöscht"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> gelöscht"
@@ -993,7 +1011,7 @@ msgstr "Morgen fällig"
msgid "Edit"
msgstr "Bearbeiten"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Board-URL bearbeiten"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Loslegen"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Beginnen Sie, indem Sie eine(n) neue(n) {0} erstellen"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Beginne mit dem Erstellen einer neuen Liste"
@@ -1595,7 +1613,7 @@ msgstr "Listenname"
msgid "Lists"
msgstr "Listen"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Weitere Aktivitäten laden"
@@ -1603,7 +1621,7 @@ msgstr "Weitere Aktivitäten laden"
msgid "Loading permissions..."
msgstr "Berechtigungen werden geladen..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Wird geladen..."
@@ -1631,16 +1649,16 @@ msgstr "Niedrige Priorität"
msgid "magic link"
msgstr "Magic Link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Als Vorlage speichern"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "hat ein Checklistenelement als unvollständig markiert"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "hat Checklistenelement <0>{0}</0> als unvollständig markiert"
@@ -1648,6 +1666,8 @@ msgstr "hat Checklistenelement <0>{0}</0> als unvollständig markiert"
msgid "Medium Priority"
msgstr "Mittlere Priorität"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Mitglied"
@@ -1676,11 +1696,11 @@ msgstr "monatliche Abrechnung"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "hat die Karte von <0>{0}</0> nach <1>{1}</1> verschoben"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "hat die Karte in eine andere Liste verschoben"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Nächste Schritte"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Keine {0}"
@@ -1790,11 +1810,11 @@ msgstr "Für diesen Anhang ist keine Download-URL verfügbar."
msgid "No keyboard shortcuts registered."
msgstr "Keine Tastaturkürzel registriert."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Keine Listen"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Es wurden noch keine Listen erstellt"
@@ -1969,6 +1989,7 @@ msgstr "Bitte wähle eine Datei zum Hochladen aus."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remote"
msgid "Remove"
msgstr "Entfernen"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Aus Favoriten entfernen"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Mitglied entfernen"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "{0} Labels entfernt: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "hat ein Label von der Karte entfernt"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "hat ein Mitglied von der Karte entfernt"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Aus Favoriten entfernt"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "hat Label <0>{0}</0> entfernt"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "hat den Fälligkeitstermin entfernt"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "hat eine Checkliste umbenannt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> umbenannt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "hat Checklistenelement in <0>{0}</0> umbenannt"
@@ -2219,7 +2248,7 @@ msgstr "Selbst hosten"
msgid "Self host with Github"
msgstr "Selbst hosten mit Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "hat sich selbst die Karte zugewiesen"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Fälligkeitsdatum festlegen"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "hat den Fälligkeitstermin festgelegt"
@@ -2603,6 +2632,10 @@ msgstr "Berechtigungen können nicht zurückgesetzt werden"
msgid "Unable to send feedback"
msgstr "Feedback konnte nicht gesendet werden"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Tafel konnte nicht aktualisiert werden"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Board-URL kann nicht aktualisiert werden"
@@ -2654,12 +2687,12 @@ msgstr "Berechtigungen können nicht aktualisiert werden"
msgid "Unable to update role"
msgstr "Rolle kann nicht aktualisiert werden"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "hat <0>{0}</0> von der Karte entfernt"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "hat sich selbst von der Karte entfernt"
@@ -2718,24 +2751,24 @@ msgstr "Aktualisieren"
msgid "Update label"
msgstr "Label aktualisieren"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "hat ein Checklistenelement aktualisiert"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "hat die Beschreibung aktualisiert"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "hat den Fälligkeitstermin aktualisiert"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "hat den Titel aktualisiert"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "hat den Titel in <0>{0}</0> geändert"
@@ -2860,7 +2893,7 @@ msgstr "Warum ein open source Trello entwickeln?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Arbeitsbereich"
@@ -2965,8 +2998,8 @@ msgstr "Sie können selbst hosten, indem sie den anweisungen in unserem <0>repo<
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} has been added to your favorites."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} has been removed from your favorites."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -147,39 +157,43 @@ msgstr "Add label"
msgid "Add member"
msgstr "Add member"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Add to favorites"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "added {0} labels: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "added a checklist"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "added a checklist item"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "added a label to the card"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "added a member to the card"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "added checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "added checklist item <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "added label <0>{0}</0>"
@@ -187,6 +201,10 @@ msgstr "added label <0>{0}</0>"
#~ msgid "added label <0>{label}</0>"
#~ msgstr "added label <0>{label}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Added to favorites"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Adding a new member will cost an additional {price} ({billingType}) per seat."
@@ -314,8 +332,8 @@ msgstr "Are you sure you want to delete your account?"
#~ msgid "around the world"
#~ msgstr "around the world"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "assigned <0>{0}</0> to the card"
@@ -623,8 +641,8 @@ msgstr "Change Password"
msgid "Change your language preferences."
msgstr "Change your language preferences."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "changed the due date to <0>{formattedDate}</0>"
@@ -712,12 +730,12 @@ msgstr "Complete"
msgid "Complete control and ownership:"
msgstr "Complete control and ownership:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "completed a checklist item"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "completed checklist item <0>{0}</0>"
@@ -828,7 +846,7 @@ msgid "Create list"
msgstr "Create list"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Create new {0}"
@@ -847,7 +865,7 @@ msgid "Create new label"
msgstr "Create new label"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Create new list"
@@ -864,7 +882,7 @@ msgstr "Create template"
msgid "Create workspace"
msgstr "Create workspace"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "created the card"
@@ -946,7 +964,7 @@ msgstr "Delete"
msgid "Delete account"
msgstr "Delete account"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Delete board"
@@ -962,7 +980,7 @@ msgstr "Delete comment"
msgid "Delete list"
msgstr "Delete list"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Delete template"
@@ -972,21 +990,21 @@ msgstr "Delete template"
msgid "Delete workspace"
msgstr "Delete workspace"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "deleted a checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "deleted a checklist item"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "deleted checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "deleted checklist item <0>{0}</0>"
@@ -1082,7 +1100,7 @@ msgstr "Due tomorrow"
msgid "Edit"
msgstr "Edit"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Edit board URL"
@@ -1378,7 +1396,7 @@ msgid "Get Started"
msgstr "Get Started"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Get started by creating a new {0}"
@@ -1386,7 +1404,7 @@ msgstr "Get started by creating a new {0}"
#~ msgid "Get started by creating a new board"
#~ msgstr "Get started by creating a new board"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Get started by creating a new list"
@@ -1704,7 +1722,7 @@ msgstr "List name"
msgid "Lists"
msgstr "Lists"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Load more activities"
@@ -1712,7 +1730,7 @@ msgstr "Load more activities"
msgid "Loading permissions..."
msgstr "Loading permissions..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Loading..."
@@ -1740,16 +1758,16 @@ msgstr "Low Priority"
msgid "magic link"
msgstr "magic link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Make template"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "marked a checklist item as incomplete"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marked checklist item <0>{0}</0> as incomplete"
@@ -1757,6 +1775,8 @@ msgstr "marked checklist item <0>{0}</0> as incomplete"
msgid "Medium Priority"
msgstr "Medium Priority"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Member"
@@ -1785,7 +1805,7 @@ msgstr "monthly billing"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "moved the card from <0>{0}</0> to<1>{1}</1>"
@@ -1793,7 +1813,7 @@ msgstr "moved the card from <0>{0}</0> to<1>{1}</1>"
#~ msgid "moved the card from <0>{fromList}</0> to<1>{toList}</1>"
#~ msgstr "moved the card from <0>{fromList}</0> to<1>{toList}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "moved the card to another list"
@@ -1887,7 +1907,7 @@ msgid "Next Steps"
msgstr "Next Steps"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "No {0}"
@@ -1919,11 +1939,11 @@ msgstr "No download URL available for this attachment."
msgid "No keyboard shortcuts registered."
msgstr "No keyboard shortcuts registered."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "No lists"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "No lists have been created yet"
@@ -2098,6 +2118,7 @@ msgstr "Please select a file to upload."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2225,25 +2246,33 @@ msgstr "Remote"
msgid "Remove"
msgstr "Remove"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Remove from favorites"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Remove member"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "removed {0} labels: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "removed a label from the card"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "removed a member from the card"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Removed from favorites"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "removed label <0>{0}</0>"
@@ -2251,22 +2280,22 @@ msgstr "removed label <0>{0}</0>"
#~ msgid "removed label <0>{label}</0>"
#~ msgstr "removed label <0>{label}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "removed the due date"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "renamed a checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "renamed checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renamed checklist item to <0>{0}</0>"
@@ -2356,7 +2385,7 @@ msgstr "Self Host"
msgid "Self host with Github"
msgstr "Self host with Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "self-assigned the card"
@@ -2380,7 +2409,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Set due date"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "set the due date"
@@ -2760,6 +2789,10 @@ msgstr "Unable to reset permissions"
msgid "Unable to send feedback"
msgstr "Unable to send feedback"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Unable to update board"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Unable to update board URL"
@@ -2811,8 +2844,8 @@ msgstr "Unable to update permissions"
msgid "Unable to update role"
msgstr "Unable to update role"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "unassigned <0>{0}</0> from the card"
@@ -2820,7 +2853,7 @@ msgstr "unassigned <0>{0}</0> from the card"
#~ msgid "unassigned <0>{memberName}</0> from the card"
#~ msgstr "unassigned <0>{memberName}</0> from the card"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "unassigned themselves from the card"
@@ -2879,24 +2912,24 @@ msgstr "Update"
msgid "Update label"
msgstr "Update label"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "updated a checklist item"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "updated the description"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "updated the due date"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "updated the title"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "updated the title to <0>{0}</0>"
@@ -3041,7 +3074,7 @@ msgstr "Why make an open source Trello?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Workspace"
@@ -3150,8 +3183,8 @@ msgstr "You can self-host by following the instructions in our <0>repo</0>."
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} se ha añadido a tus favoritos."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} se ha eliminado de tus favoritos."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Añadir etiqueta"
msgid "Add member"
msgstr "Añadir miembro"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Añadir a favoritos"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "añadió {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "añadió una lista de verificación"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "añadió un elemento a la lista de verificación"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "añadió una etiqueta a la tarjeta"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "añadió un miembro a la tarjeta"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "añadió la lista de verificación <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "añadió el elemento <0>{0}</0> a la lista de verificación"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "añadió la etiqueta <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Añadido a favoritos"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Añadir un nuevo miembro costará {price} adicionales ({billingType}) por asiento."
@@ -282,8 +300,8 @@ msgstr "¿Estás seguro de que quieres eliminar esta etiqueta?"
msgid "Are you sure you want to delete your account?"
msgstr "¿Estás seguro de que quieres eliminar tu cuenta?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "asignó <0>{0}</0> a la tarjeta"
@@ -562,8 +580,8 @@ msgstr "Cambiar contraseña"
msgid "Change your language preferences."
msgstr "Cambia tus preferencias de idioma."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "cambió la fecha de vencimiento a <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Completado"
msgid "Complete control and ownership:"
msgstr "Control y propiedad completos:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "completó un elemento de la lista de verificación"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "completó el elemento <0>{0}</0> de la lista de verificación"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Crear lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Crear nuevo {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Crear nueva etiqueta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Crear nueva lista"
@@ -783,7 +801,7 @@ msgstr "Crear plantilla"
msgid "Create workspace"
msgstr "Crear espacio de trabajo"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "creó la tarjeta"
@@ -857,7 +875,7 @@ msgstr "Eliminar"
msgid "Delete account"
msgstr "Eliminar cuenta"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Eliminar tablero"
@@ -873,7 +891,7 @@ msgstr "Eliminar comentario"
msgid "Delete list"
msgstr "Eliminar lista"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Eliminar plantilla"
@@ -883,21 +901,21 @@ msgstr "Eliminar plantilla"
msgid "Delete workspace"
msgstr "Eliminar espacio de trabajo"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "eliminó una lista de verificación"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "eliminó un elemento de la lista de verificación"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "eliminó la lista de verificación <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "eliminó el elemento <0>{0}</0> de la lista de verificación"
@@ -993,7 +1011,7 @@ msgstr "Vence mañana"
msgid "Edit"
msgstr "Editar"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Editar URL del tablero"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Comenzar"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Comienza creando un nuevo {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Comienza creando una nueva lista"
@@ -1595,7 +1613,7 @@ msgstr "Nombre de la lista"
msgid "Lists"
msgstr "Listas"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Cargar más actividades"
@@ -1603,7 +1621,7 @@ msgstr "Cargar más actividades"
msgid "Loading permissions..."
msgstr "Cargando permisos..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Cargando..."
@@ -1631,16 +1649,16 @@ msgstr "Prioridad baja"
msgid "magic link"
msgstr "enlace mágico"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Crear plantilla"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "marcó un elemento de la lista de verificación como incompleto"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marcó el elemento <0>{0}</0> de la lista de verificación como incompleto"
@@ -1648,6 +1666,8 @@ msgstr "marcó el elemento <0>{0}</0> de la lista de verificación como incomple
msgid "Medium Priority"
msgstr "Prioridad media"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Miembro"
@@ -1676,11 +1696,11 @@ msgstr "facturación mensual"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "movió la tarjeta de <0>{0}</0> a<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "movió la tarjeta a otra lista"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Próximos pasos"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Sin {0}"
@@ -1790,11 +1810,11 @@ msgstr "No hay URL de descarga disponible para este archivo adjunto."
msgid "No keyboard shortcuts registered."
msgstr "No hay atajos de teclado registrados."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Sin listas"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Aún no se han creado listas"
@@ -1969,6 +1989,7 @@ msgstr "Por favor selecciona un archivo para subir."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Eliminar"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Eliminar de favoritos"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Eliminar miembro"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "eliminó {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "eliminó una etiqueta de la tarjeta"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "eliminó un miembro de la tarjeta"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Eliminado de favoritos"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "eliminó la etiqueta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "eliminó la fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "renombró una lista de verificación"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "renombró la lista de verificación <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renombró el elemento de la lista de verificación a <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Autoalojamiento"
msgid "Self host with Github"
msgstr "Autoalojamiento con Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "se autoasignó la tarjeta"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Establecer fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "estableció la fecha de vencimiento"
@@ -2603,6 +2632,10 @@ msgstr "No se pueden restablecer los permisos"
msgid "Unable to send feedback"
msgstr "No se pudieron enviar los comentarios"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "No se pudo actualizar el tablero"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "No se puede actualizar la URL del tablero"
@@ -2654,12 +2687,12 @@ msgstr "No se pueden actualizar los permisos"
msgid "Unable to update role"
msgstr "No se puede actualizar el rol"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "desasignó <0>{0}</0> de la tarjeta"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "se desasignó de la tarjeta"
@@ -2718,24 +2751,24 @@ msgstr "Actualizar"
msgid "Update label"
msgstr "Actualizar etiqueta"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "actualizó un elemento de la lista de verificación"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "actualizó la descripción"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "actualizó la fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "actualizó el título"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "actualizó el título a <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "¿Por qué crear un Trello de código abierto?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Espacio de trabajo"
@@ -2965,8 +2998,8 @@ msgstr "Puedes autoalojar siguiendo las instrucciones en nuestro <0>repositorio<
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} a été ajouté à vos favoris."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} a été retiré de vos favoris."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Ajouter une étiquette"
msgid "Add member"
msgstr "Ajouter un membre"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Ajouter aux favoris"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "a ajouté {0} étiquettes : <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "a ajouté une checklist"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "a ajouté un élément à la checklist"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "a ajouté une étiquette à la carte"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "a ajouté un membre à la carte"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "a ajouté la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "a ajouté l'élément <0>{0}</0> à la checklist"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "a ajouté l'étiquette <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Ajouté aux favoris"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'ajout d'un nouveau membre coûtera {price} supplémentaires ({billingType}) par siège."
@@ -282,8 +300,8 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette étiquette ?"
msgid "Are you sure you want to delete your account?"
msgstr "Êtes-vous sûr de vouloir supprimer votre compte ?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "a assigné <0>{0}</0> à la carte"
@@ -562,8 +580,8 @@ msgstr "Modifier le mot de passe"
msgid "Change your language preferences."
msgstr "Modifiez vos préférences linguistiques."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "a modifié la date d'échéance pour <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Terminé"
msgid "Complete control and ownership:"
msgstr "Contrôle et propriété complets :"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "a terminé un élément de la checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "a terminé l'élément <0>{0}</0> de la checklist"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Créer une liste"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Créer un nouveau {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Créer une nouvelle étiquette"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Créer une nouvelle liste"
@@ -783,7 +801,7 @@ msgstr "Créer un modèle"
msgid "Create workspace"
msgstr "Créer un espace de travail"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "a créé la carte"
@@ -857,7 +875,7 @@ msgstr "Supprimer"
msgid "Delete account"
msgstr "Supprimer le compte"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Supprimer le tableau"
@@ -873,7 +891,7 @@ msgstr "Supprimer le commentaire"
msgid "Delete list"
msgstr "Supprimer la liste"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Supprimer le modèle"
@@ -883,21 +901,21 @@ msgstr "Supprimer le modèle"
msgid "Delete workspace"
msgstr "Supprimer l'espace de travail"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "a supprimé une checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "a supprimé un élément de la checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "a supprimé la checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "a supprimé l'élément <0>{0}</0> de la checklist"
@@ -993,7 +1011,7 @@ msgstr "Échéance demain"
msgid "Edit"
msgstr "Modifier"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Modifier l'URL du tableau"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Commencer"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Commencez par créer un nouveau {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Commencez par créer une nouvelle liste"
@@ -1595,7 +1613,7 @@ msgstr "Nom de la liste"
msgid "Lists"
msgstr "Listes"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Charger plus d'activités"
@@ -1603,7 +1621,7 @@ msgstr "Charger plus d'activités"
msgid "Loading permissions..."
msgstr "Chargement des permissions..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Chargement..."
@@ -1631,16 +1649,16 @@ msgstr "Priorité basse"
msgid "magic link"
msgstr "lien magique"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Créer un modèle"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "a marqué un élément de la checklist comme incomplet"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "a marqué l'élément <0>{0}</0> de la checklist comme incomplet"
@@ -1648,6 +1666,8 @@ msgstr "a marqué l'élément <0>{0}</0> de la checklist comme incomplet"
msgid "Medium Priority"
msgstr "Priorité moyenne"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Membre"
@@ -1676,11 +1696,11 @@ msgstr "facturation mensuelle"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "a déplacé la carte de <0>{0}</0> vers <1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "a déplacé la carte vers une autre liste"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Prochaines étapes"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Aucun {0}"
@@ -1790,11 +1810,11 @@ msgstr "Aucune URL de téléchargement disponible pour cette pièce jointe."
msgid "No keyboard shortcuts registered."
msgstr "Aucun raccourci clavier enregistré."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Aucune liste"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Aucune liste n'a encore été créée"
@@ -1969,6 +1989,7 @@ msgstr "Veuillez sélectionner un fichier à télécharger."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "À distance"
msgid "Remove"
msgstr "Supprimer"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Retirer des favoris"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Supprimer le membre"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "a supprimé {0} étiquettes : <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "a supprimé une étiquette de la carte"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "a retiré un membre de la carte"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Retiré des favoris"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "a supprimé l'étiquette <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "a supprimé la date d'échéance"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "a renommé une checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "a renommé la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "a renommé l'élément de la liste de contrôle en <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Auto-hébergement"
msgid "Self host with Github"
msgstr "Auto-hébergement avec Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "s'est auto-assigné la carte"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Définir la date d'échéance"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "a défini la date d'échéance"
@@ -2603,6 +2632,10 @@ msgstr "Impossible de réinitialiser les permissions"
msgid "Unable to send feedback"
msgstr "Impossible d'envoyer les commentaires"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Impossible de mettre à jour le tableau"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Impossible de mettre à jour l'URL du tableau"
@@ -2654,12 +2687,12 @@ msgstr "Impossible de mettre à jour les permissions"
msgid "Unable to update role"
msgstr "Impossible de mettre à jour le rôle"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "a désassigné <0>{0}</0> de la carte"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "s'est retiré de la carte"
@@ -2718,24 +2751,24 @@ msgstr "Mettre à jour"
msgid "Update label"
msgstr "Mettre à jour l'étiquette"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "a mis à jour un élément de la liste de contrôle"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "a mis à jour la description"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "a mis à jour la date d'échéance"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "a mis à jour le titre"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "a mis à jour le titre en <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Pourquoi créer un Trello open source ?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Espace de travail"
@@ -2965,8 +2998,8 @@ msgstr "Vous pouvez auto-héberger en suivant les instructions dans notre <0>dé
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} è stato aggiunto ai tuoi preferiti."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} è stato rimosso dai tuoi preferiti."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Aggiungi etichetta"
msgid "Add member"
msgstr "Aggiungi membro"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Aggiungi ai preferiti"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "ha aggiunto {0} etichette: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "ha aggiunto una checklist"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "ha aggiunto un elemento alla checklist"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "ha aggiunto un'etichetta alla carta"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "ha aggiunto un membro alla carta"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "ha aggiunto la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "ha aggiunto l'elemento <0>{0}</0> alla checklist"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "ha aggiunto l'etichetta <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Aggiunto ai preferiti"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'aggiunta di un nuovo membro costerà un supplemento di {price} ({billingType}) per posto."
@@ -282,8 +300,8 @@ msgstr "Sei sicuro di voler eliminare questa etichetta?"
msgid "Are you sure you want to delete your account?"
msgstr "Sei sicuro di voler eliminare il tuo account?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "ha assegnato <0>{0}</0> alla scheda"
@@ -562,8 +580,8 @@ msgstr "Cambia password"
msgid "Change your language preferences."
msgstr "Modifica le tue preferenze di lingua."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "ha cambiato la data di scadenza a <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Completato"
msgid "Complete control and ownership:"
msgstr "Controllo e proprietà completi:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "ha completato un elemento della checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "ha completato l'elemento <0>{0}</0> della checklist"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Crea lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Crea nuovo {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Crea nuova etichetta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Crea nuova lista"
@@ -783,7 +801,7 @@ msgstr "Crea template"
msgid "Create workspace"
msgstr "Crea spazio di lavoro"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "ha creato la carta"
@@ -857,7 +875,7 @@ msgstr "Elimina"
msgid "Delete account"
msgstr "Elimina account"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Elimina bacheca"
@@ -873,7 +891,7 @@ msgstr "Elimina commento"
msgid "Delete list"
msgstr "Elimina lista"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Elimina template"
@@ -883,21 +901,21 @@ msgstr "Elimina template"
msgid "Delete workspace"
msgstr "Elimina spazio di lavoro"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "ha eliminato una checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "ha eliminato un elemento della checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "ha eliminato la checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "ha eliminato l'elemento <0>{0}</0> della checklist"
@@ -993,7 +1011,7 @@ msgstr "Scade domani"
msgid "Edit"
msgstr "Modifica"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Modifica URL della bacheca"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Inizia ora"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Inizia creando un nuovo {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Inizia creando una nuova lista"
@@ -1595,7 +1613,7 @@ msgstr "Nome lista"
msgid "Lists"
msgstr "Liste"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Carica altre attività"
@@ -1603,7 +1621,7 @@ msgstr "Carica altre attività"
msgid "Loading permissions..."
msgstr "Caricamento permessi..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Caricamento in corso..."
@@ -1631,16 +1649,16 @@ msgstr "Bassa priorità"
msgid "magic link"
msgstr "link magico"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Crea template"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "ha segnato un elemento della checklist come incompleto"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "ha segnato l'elemento <0>{0}</0> della checklist come incompleto"
@@ -1648,6 +1666,8 @@ msgstr "ha segnato l'elemento <0>{0}</0> della checklist come incompleto"
msgid "Medium Priority"
msgstr "Media priorità"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Membro"
@@ -1676,11 +1696,11 @@ msgstr "fatturazione mensile"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "ha spostato la scheda da <0>{0}</0> a<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "ha spostato la scheda in un'altra lista"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Prossimi passi"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Nessun {0}"
@@ -1790,11 +1810,11 @@ msgstr "Nessun URL di download disponibile per questo allegato."
msgid "No keyboard shortcuts registered."
msgstr "Nessuna scorciatoia da tastiera registrata."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Nessuna lista"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Nessuna lista è stata ancora creata"
@@ -1969,6 +1989,7 @@ msgstr "Seleziona un file da caricare."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Rimuovi"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Rimuovi dai preferiti"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Rimuovi membro"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "ha rimosso {0} etichette: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "ha rimosso un'etichetta dalla scheda"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "ha rimosso un membro dalla scheda"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Rimosso dai preferiti"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "ha rimosso l'etichetta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "ha rimosso la data di scadenza"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "ha rinominato una checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "ha rinominato la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "ha rinominato l'elemento della checklist in <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Self Host"
msgid "Self host with Github"
msgstr "Self host con Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "si è auto-assegnato la scheda"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Imposta data di scadenza"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "ha impostato la data di scadenza"
@@ -2603,6 +2632,10 @@ msgstr "Impossibile ripristinare i permessi"
msgid "Unable to send feedback"
msgstr "Impossibile inviare feedback"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Impossibile aggiornare la bacheca"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Impossibile aggiornare l'URL della bacheca"
@@ -2654,12 +2687,12 @@ msgstr "Impossibile aggiornare i permessi"
msgid "Unable to update role"
msgstr "Impossibile aggiornare il ruolo"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "ha rimosso l'assegnazione di <0>{0}</0> dalla scheda"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "si è rimosso dall'assegnazione della scheda"
@@ -2718,24 +2751,24 @@ msgstr "Aggiorna"
msgid "Update label"
msgstr "Aggiorna etichetta"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "ha aggiornato un elemento della checklist"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "ha aggiornato la descrizione"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "ha aggiornato la data di scadenza"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "ha aggiornato il titolo"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "ha aggiornato il titolo in <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Perché creare un Trello open source?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Spazio di lavoro"
@@ -2965,8 +2998,8 @@ msgstr "Puoi effettuare il self-hosting seguendo le istruzioni nel nostro <0>rep
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} is toegevoegd aan je favorieten."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} is verwijderd uit je favorieten."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Label toevoegen"
msgid "Add member"
msgstr "Lid toevoegen"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Toevoegen aan favorieten"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "{0} labels toegevoegd: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "heeft een checklist toegevoegd"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "heeft een checklistitem toegevoegd"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "heeft een label aan de kaart toegevoegd"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "heeft een lid aan de kaart toegevoegd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> toegevoegd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> toegevoegd"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "heeft label <0>{0}</0> toegevoegd"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Toegevoegd aan favorieten"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Het toevoegen van een nieuw lid kost een extra {price} ({billingType}) per plaats."
@@ -282,8 +300,8 @@ msgstr "Weet je zeker dat je dit label wilt verwijderen?"
msgid "Are you sure you want to delete your account?"
msgstr "Weet je zeker dat je je account wilt verwijderen?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "heeft <0>{0}</0> aan de kaart toegewezen"
@@ -562,8 +580,8 @@ msgstr "Wachtwoord wijzigen"
msgid "Change your language preferences."
msgstr "Wijzig je taalvoorkeuren."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "heeft de vervaldatum gewijzigd naar <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Voltooid"
msgid "Complete control and ownership:"
msgstr "Volledige controle en eigendom:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "heeft een checklistitem voltooid"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> voltooid"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Lijst maken"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Nieuwe {0} maken"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Maak nieuw label"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Maak nieuwe lijst"
@@ -783,7 +801,7 @@ msgstr "Sjabloon maken"
msgid "Create workspace"
msgstr "Werkruimte maken"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "heeft de kaart aangemaakt"
@@ -857,7 +875,7 @@ msgstr "Verwijderen"
msgid "Delete account"
msgstr "Account verwijderen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Board verwijderen"
@@ -873,7 +891,7 @@ msgstr "Reactie verwijderen"
msgid "Delete list"
msgstr "Lijst verwijderen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Sjabloon verwijderen"
@@ -883,21 +901,21 @@ msgstr "Sjabloon verwijderen"
msgid "Delete workspace"
msgstr "Werkruimte verwijderen"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "heeft een checklist verwijderd"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "heeft een checklistitem verwijderd"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> verwijderd"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> verwijderd"
@@ -993,7 +1011,7 @@ msgstr "Vervalt morgen"
msgid "Edit"
msgstr "Bewerken"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Board URL bewerken"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Aan de slag"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Begin door een nieuwe {0} te maken"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Begin door een nieuwe lijst te maken"
@@ -1595,7 +1613,7 @@ msgstr "Lijstnaam"
msgid "Lists"
msgstr "Lijsten"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Meer activiteiten laden"
@@ -1603,7 +1621,7 @@ msgstr "Meer activiteiten laden"
msgid "Loading permissions..."
msgstr "Rechten laden..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Laden..."
@@ -1631,16 +1649,16 @@ msgstr "Lage prioriteit"
msgid "magic link"
msgstr "magische link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Sjabloon maken"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "heeft een checklistitem als onvoltooid gemarkeerd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "heeft checklistitem <0>{0}</0> als onvoltooid gemarkeerd"
@@ -1648,6 +1666,8 @@ msgstr "heeft checklistitem <0>{0}</0> als onvoltooid gemarkeerd"
msgid "Medium Priority"
msgstr "Gemiddelde prioriteit"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Lid"
@@ -1676,11 +1696,11 @@ msgstr "maandelijkse facturering"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "heeft de kaart verplaatst van <0>{0}</0> naar<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "heeft de kaart naar een andere lijst verplaatst"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Volgende stappen"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Geen {0}"
@@ -1790,11 +1810,11 @@ msgstr "Geen download-URL beschikbaar voor deze bijlage."
msgid "No keyboard shortcuts registered."
msgstr "Geen sneltoetsen geregistreerd."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Geen lijsten"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Er zijn nog geen lijsten aangemaakt"
@@ -1969,6 +1989,7 @@ msgstr "Selecteer een bestand om te uploaden."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Op afstand"
msgid "Remove"
msgstr "Verwijderen"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Verwijderen uit favorieten"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Lid verwijderen"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "{0} labels verwijderd: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "heeft een label van de kaart verwijderd"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "heeft een lid van de kaart verwijderd"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Verwijderd uit favorieten"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "heeft label <0>{0}</0> verwijderd"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "heeft de vervaldatum verwijderd"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "heeft een checklist hernoemd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> hernoemd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "checklistitem hernoemd naar <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Zelf hosten"
msgid "Self host with Github"
msgstr "Zelf hosten met Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "heeft de kaart aan zichzelf toegewezen"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Vervaldatum instellen"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "heeft de vervaldatum ingesteld"
@@ -2603,6 +2632,10 @@ msgstr "Kan machtigingen niet resetten"
msgid "Unable to send feedback"
msgstr "Feedback kon niet worden verzonden"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Kan bord niet bijwerken"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Kan board URL niet bijwerken"
@@ -2654,12 +2687,12 @@ msgstr "Kan machtigingen niet bijwerken"
msgid "Unable to update role"
msgstr "Kan rol niet bijwerken"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "<0>{0}</0> van de kaart verwijderd"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "zichzelf van de kaart verwijderd"
@@ -2718,24 +2751,24 @@ msgstr "Bijwerken"
msgid "Update label"
msgstr "Label bijwerken"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "een checklistitem bijgewerkt"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "beschrijving bijgewerkt"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "heeft de vervaldatum bijgewerkt"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "titel bijgewerkt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "titel bijgewerkt naar <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Waarom een open source Trello maken?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Werkruimte"
@@ -2965,8 +2998,8 @@ msgstr "Je kunt zelf hosten door de instructies in onze <0>repo</0> te volgen."
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -40,6 +40,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} zostało dodane do ulubionych."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} zostało usunięte z ulubionych."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -136,42 +146,50 @@ msgstr "Dodaj etykietę"
msgid "Add member"
msgstr "Dodaj członka"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Dodaj do ulubionych"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "dodano {0} etykiety: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "dodał listę kontrolną"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "dodał(a) element listy kontrolnej"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "dodał(a) etykietę do karty"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "dodał(a) członka do karty"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "dodał(a) listę kontrolną <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "dodał(a) element listy kontrolnej <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "dodał(a) etykietę <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Dodano do ulubionych"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Dodanie nowego członka będzie kosztować dodatkowe {price} ({billingType}) za miejsce."
@@ -287,8 +305,8 @@ msgstr "Czy na pewno chcesz usunąć tę etykietę?"
msgid "Are you sure you want to delete your account?"
msgstr "Czy na pewno chcesz usunąć swoje konto?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "przypisano <0>{0}</0> do karty"
@@ -567,8 +585,8 @@ msgstr "Zmień hasło"
msgid "Change your language preferences."
msgstr "Zmień swoje preferencje językowe."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "zmieniono termin na <0>{formattedDate}</0>"
@@ -656,12 +674,12 @@ msgstr "Zakończ"
msgid "Complete control and ownership:"
msgstr "Pełna kontrola i własność:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "ukończono element listy kontrolnej"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "ukończono element listy kontrolnej <0>{0}</0>"
@@ -760,7 +778,7 @@ msgid "Create list"
msgstr "Utwórz listę"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Utwórz nowy {0}"
@@ -775,7 +793,7 @@ msgid "Create new label"
msgstr "Utwórz nową etykietę"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Utwórz nową listę"
@@ -788,7 +806,7 @@ msgstr "Utwórz szablon"
msgid "Create workspace"
msgstr "Utwórz przestrzeń roboczą"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "utworzył(a) kartę"
@@ -862,7 +880,7 @@ msgstr "Usuń"
msgid "Delete account"
msgstr "Usuń konto"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Usuń tablicę"
@@ -878,7 +896,7 @@ msgstr "Usuń komentarz"
msgid "Delete list"
msgstr "Usuń listę"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Usuń szablon"
@@ -888,21 +906,21 @@ msgstr "Usuń szablon"
msgid "Delete workspace"
msgstr "Usuń przestrzeń roboczą"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "usunięto listę kontrolną"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "usunięto element listy kontrolnej"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "usunięto listę kontrolną <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "usunięto element listy kontrolnej <0>{0}</0>"
@@ -998,7 +1016,7 @@ msgstr "Termin na jutro"
msgid "Edit"
msgstr "Edytuj"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Edytuj URL tablicy"
@@ -1286,11 +1304,11 @@ msgid "Get Started"
msgstr "Rozpocznij"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Rozpocznij, tworząc nowy {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Rozpocznij, tworząc nową listę"
@@ -1600,7 +1618,7 @@ msgstr "Nazwa listy"
msgid "Lists"
msgstr "Listy"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Załaduj więcej aktywności"
@@ -1608,7 +1626,7 @@ msgstr "Załaduj więcej aktywności"
msgid "Loading permissions..."
msgstr "Ładowanie uprawnień..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Ładowanie..."
@@ -1636,16 +1654,16 @@ msgstr "Niski priorytet"
msgid "magic link"
msgstr "magiczny link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Utwórz szablon"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "oznaczył element listy kontrolnej jako nieukończony"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "oznaczył element listy kontrolnej <0>{0}</0> jako nieukończony"
@@ -1653,6 +1671,8 @@ msgstr "oznaczył element listy kontrolnej <0>{0}</0> jako nieukończony"
msgid "Medium Priority"
msgstr "Średni priorytet"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Członek"
@@ -1681,11 +1701,11 @@ msgstr "rozliczenie miesięczne"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "przeniesiono kartę z <0>{0}</0> do<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "przeniesiono kartę do innej listy"
@@ -1767,7 +1787,7 @@ msgid "Next Steps"
msgstr "Następne kroki"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Brak {0}"
@@ -1795,11 +1815,11 @@ msgstr "Brak dostępnego URL do pobrania tego załącznika."
msgid "No keyboard shortcuts registered."
msgstr "Brak zarejestrowanych skrótów klawiaturowych."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Brak list"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Nie utworzono jeszcze żadnych list"
@@ -1974,6 +1994,7 @@ msgstr "Proszę wybrać plik do przesłania."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2101,44 +2122,52 @@ msgstr "Zdalne"
msgid "Remove"
msgstr "Usuń"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Usuń z ulubionych"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Usuń członka"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "usunięto {0} etykiety: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "usunął etykietę z karty"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "usunął członka z karty"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Usunięto z ulubionych"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "usunięto etykietę <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "usunięto termin"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "zmieniono nazwę listy kontrolnej"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "zmieniono nazwę listy kontrolnej <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "zmieniono nazwę elementu listy kontrolnej na <0>{0}</0>"
@@ -2224,7 +2253,7 @@ msgstr "Hostuj samodzielnie"
msgid "Self host with Github"
msgstr "Hostuj samodzielnie z Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "przypisał(a) sobie kartę"
@@ -2244,7 +2273,7 @@ msgstr "Starszy"
msgid "Set due date"
msgstr "Ustaw termin"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "ustawiono termin"
@@ -2608,6 +2637,10 @@ msgstr "Nie można zresetować uprawnień"
msgid "Unable to send feedback"
msgstr "Nie można wysłać opinii"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Nie można zaktualizować tablicy"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Nie można zaktualizować adresu URL tablicy"
@@ -2659,12 +2692,12 @@ msgstr "Nie można zaktualizować uprawnień"
msgid "Unable to update role"
msgstr "Nie można zaktualizować roli"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "usunięto przypisanie <0>{0}</0> z karty"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "usunęli swoje przypisanie z karty"
@@ -2723,24 +2756,24 @@ msgstr "Aktualizuj"
msgid "Update label"
msgstr "Zaktualizuj etykietę"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "zaktualizowano element listy kontrolnej"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "zaktualizowano opis"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "zaktualizowano termin"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "zaktualizowano tytuł"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "zaktualizowano tytuł na <0>{0}</0>"
@@ -2865,7 +2898,7 @@ msgstr "Dlaczego próbujecie stworzyć Trello w wersji otwartoźródłowej?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Obszar roboczy"
@@ -2970,8 +3003,8 @@ msgstr "Możesz hostować samodzielnie, postępując zgodnie z instrukcjami w na
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} foi adicionado aos seus favoritos."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} foi removido dos seus favoritos."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Adicionar etiqueta"
msgid "Add member"
msgstr "Adicionar membro"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Adicionar aos favoritos"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "adicionou {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "adicionou uma lista de verificação"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "adicionou um item da lista de verificação"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "adicionou uma etiqueta ao cartão"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "adicionou um membro ao cartão"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "adicionou a lista de verificação <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "adicionou o item da lista de verificação <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "adicionou a etiqueta <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Adicionado aos favoritos"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Adicionar um novo membro custará {price} ({billingType}) adicionais por assento."
@@ -282,8 +300,8 @@ msgstr "Tem certeza de que deseja excluir esta etiqueta?"
msgid "Are you sure you want to delete your account?"
msgstr "Tem certeza de que deseja excluir sua conta?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "atribuiu <0>{0}</0> ao card"
@@ -562,8 +580,8 @@ msgstr "Alterar senha"
msgid "Change your language preferences."
msgstr "Altere suas preferências de idioma."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "alterou a data de vencimento para <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Concluir"
msgid "Complete control and ownership:"
msgstr "Controle e propriedade completos:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "concluiu um item da lista de verificação"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "concluiu o item da lista de verificação <0>{0}</0>"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Criar lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Criar novo {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Criar nova etiqueta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Criar nova lista"
@@ -783,7 +801,7 @@ msgstr "Criar modelo"
msgid "Create workspace"
msgstr "Criar workspace"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "criou o cartão"
@@ -857,7 +875,7 @@ msgstr "Excluir"
msgid "Delete account"
msgstr "Excluir conta"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Excluir quadro"
@@ -873,7 +891,7 @@ msgstr "Excluir comentário"
msgid "Delete list"
msgstr "Excluir lista"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Excluir modelo"
@@ -883,21 +901,21 @@ msgstr "Excluir modelo"
msgid "Delete workspace"
msgstr "Excluir workspace"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "excluiu uma checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "excluiu um item da checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "excluiu a checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "excluiu o item da checklist <0>{0}</0>"
@@ -993,7 +1011,7 @@ msgstr "Vence amanhã"
msgid "Edit"
msgstr "Editar"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Editar URL do quadro"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Começar"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Comece criando um novo {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Comece criando uma nova lista"
@@ -1595,7 +1613,7 @@ msgstr "Nome da lista"
msgid "Lists"
msgstr "Listas"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Carregar mais atividades"
@@ -1603,7 +1621,7 @@ msgstr "Carregar mais atividades"
msgid "Loading permissions..."
msgstr "Carregando permissões..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Carregando..."
@@ -1631,16 +1649,16 @@ msgstr "Prioridade baixa"
msgid "magic link"
msgstr "link mágico"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Criar modelo"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "marcou um item da checklist como incompleto"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marcou o item da checklist <0>{0}</0> como incompleto"
@@ -1648,6 +1666,8 @@ msgstr "marcou o item da checklist <0>{0}</0> como incompleto"
msgid "Medium Priority"
msgstr "Prioridade média"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Membro"
@@ -1676,11 +1696,11 @@ msgstr "cobrança mensal"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "moveu o card de <0>{0}</0> para <1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "moveu o card para outra lista"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Próximos passos"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Nenhum {0}"
@@ -1790,11 +1810,11 @@ msgstr "Nenhuma URL de download disponível para este anexo."
msgid "No keyboard shortcuts registered."
msgstr "Nenhum atalho de teclado registrado."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Nenhuma lista"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Nenhuma lista foi criada ainda"
@@ -1969,6 +1989,7 @@ msgstr "Por favor, selecione um arquivo para fazer upload."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Remover"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Remover dos favoritos"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Remover membro"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "removeu {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "removeu uma etiqueta do cartão"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "removeu um membro do cartão"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Removido dos favoritos"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "removeu a etiqueta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "removeu a data de vencimento"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "renomeou uma lista de verificação"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "renomeou a checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renomeou o item da checklist para <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Auto-hospedagem"
msgid "Self host with Github"
msgstr "Auto-hospede com Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "atribuiu o cartão a si mesmo"
@@ -2239,7 +2268,7 @@ msgstr "Sênior"
msgid "Set due date"
msgstr "Definir data de vencimento"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "definiu a data de vencimento"
@@ -2603,6 +2632,10 @@ msgstr "Não foi possível redefinir as permissões"
msgid "Unable to send feedback"
msgstr "Não foi possível enviar feedback"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Não foi possível atualizar o quadro"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Não foi possível atualizar URL do quadro"
@@ -2654,12 +2687,12 @@ msgstr "Não foi possível atualizar as permissões"
msgid "Unable to update role"
msgstr "Não foi possível atualizar a função"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "removeu a atribuição de <0>{0}</0> do cartão"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "removeu a própria atribuição do cartão"
@@ -2718,24 +2751,24 @@ msgstr "Atualizar"
msgid "Update label"
msgstr "Atualizar rótulo"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "atualizou um item da lista de verificação"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "atualizou a descrição"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "atualizou a data de vencimento"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "atualizou o título"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "atualizou o título para <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Por que criar um Trello de código aberto?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Workspace"
@@ -2965,8 +2998,8 @@ msgstr "Você pode fazer self-host seguindo as instruções em nosso <0>reposit
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} добавлено в избранное."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} удалено из избранного."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Добавить метку"
msgid "Add member"
msgstr "Добавить участника"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Добавить в избранное"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "добавил {0} метки: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "добавил(а) чек-лист"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "добавил(а) элемент чек-листа"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "добавил(а) метку к карточке"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "добавил(а) участника к карточке"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "добавлен контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "добавлен элемент контрольного списка <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "добавлена метка <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Добавлено в избранное"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Добавление нового участника будет стоить дополнительно {price} ({billingType}) за место."
@@ -282,8 +300,8 @@ msgstr "Вы уверены, что хотите удалить эту метк
msgid "Are you sure you want to delete your account?"
msgstr "Вы уверены, что хотите удалить свой аккаунт?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "назначено <0>{0}</0> к карточке"
@@ -562,8 +580,8 @@ msgstr "Изменить пароль"
msgid "Change your language preferences."
msgstr "Измените настройки языка."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "изменил(а) дату выполнения на <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Завершить"
msgid "Complete control and ownership:"
msgstr "Полный контроль и владение:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "завершил элемент контрольного списка"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "завершил элемент контрольного списка <0>{0}</0>"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Создать список"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Создать новый {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Создать новую метку"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Создать новый список"
@@ -783,7 +801,7 @@ msgstr "Создать шаблон"
msgid "Create workspace"
msgstr "Создать рабочее пространство"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "создал(а) карточку"
@@ -857,7 +875,7 @@ msgstr "Удалить"
msgid "Delete account"
msgstr "Удалить аккаунт"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Удалить доску"
@@ -873,7 +891,7 @@ msgstr "Удалить комментарий"
msgid "Delete list"
msgstr "Удалить список"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Удалить шаблон"
@@ -883,21 +901,21 @@ msgstr "Удалить шаблон"
msgid "Delete workspace"
msgstr "Удалить рабочее пространство"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "удалил контрольный список"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "удалил элемент контрольного списка"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "удалил контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "удалил элемент контрольного списка <0>{0}</0>"
@@ -993,7 +1011,7 @@ msgstr "Срок завтра"
msgid "Edit"
msgstr "Редактировать"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Изменить URL доски"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Начать"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Начните с создания нового {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Начните с создания нового списка"
@@ -1595,7 +1613,7 @@ msgstr "Название списка"
msgid "Lists"
msgstr "Списки"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Загрузить больше действий"
@@ -1603,7 +1621,7 @@ msgstr "Загрузить больше действий"
msgid "Loading permissions..."
msgstr "Загрузка разрешений..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Загрузка..."
@@ -1631,16 +1649,16 @@ msgstr "Низкий приоритет"
msgid "magic link"
msgstr "магическая ссылка"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Создать шаблон"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "отметил элемент контрольного списка как незавершённый"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "отметил элемент контрольного списка <0>{0}</0> как незавершённый"
@@ -1648,6 +1666,8 @@ msgstr "отметил элемент контрольного списка <0>{
msgid "Medium Priority"
msgstr "Средний приоритет"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Участник"
@@ -1676,11 +1696,11 @@ msgstr "ежемесячная оплата"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "переместил карточку из <0>{0}</0> в <1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "переместил карточку в другой список"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Следующие шаги"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Нет {0}"
@@ -1790,11 +1810,11 @@ msgstr "URL для загрузки этого вложения недоступ
msgid "No keyboard shortcuts registered."
msgstr "Горячие клавиши не зарегистрированы."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Нет списков"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Списки ещё не созданы"
@@ -1969,6 +1989,7 @@ msgstr "Пожалуйста, выберите файл для загрузки.
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Удалённый"
msgid "Remove"
msgstr "Удалить"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Удалить из избранного"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Удалить участника"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "удалил {0} метки: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "удалил метку с карточки"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "удалил участника из карточки"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Удалено из избранного"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "удалил метку <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "удалил(а) дату выполнения"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "переименовал контрольный список"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "переименовал контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "переименовал элемент контрольного списка в <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Самостоятельный хостинг"
msgid "Self host with Github"
msgstr "Самостоятельный хостинг с Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "самостоятельно назначил карточку"
@@ -2239,7 +2268,7 @@ msgstr "Старший"
msgid "Set due date"
msgstr "Установить дату выполнения"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "установил(а) дату выполнения"
@@ -2603,6 +2632,10 @@ msgstr "Не удалось сбросить разрешения"
msgid "Unable to send feedback"
msgstr "Не удалось отправить отзыв"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Не удалось обновить доску"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Не удалось обновить URL доски"
@@ -2654,12 +2687,12 @@ msgstr "Не удалось обновить разрешения"
msgid "Unable to update role"
msgstr "Не удалось обновить роль"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "удалил(а) <0>{0}</0> из карточки"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "удалил(а) себя из карточки"
@@ -2718,24 +2751,24 @@ msgstr "Обновить"
msgid "Update label"
msgstr "Обновить метку"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "обновлен элемент контрольного списка"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "обновлено описание"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "обновил(а) дату выполнения"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "обновлен заголовок"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "обновлен заголовок на <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Зачем создавать Trello с открытым исходны
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Рабочее пространство"
@@ -2965,8 +2998,8 @@ msgstr "Вы можете развернуть собственный хости
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,128 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { createNextApiContext } from "@kan/api/trpc";
import * as cardRepo from "@kan/db/repository/card.repo";
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
import * as cardAttachmentRepo from "@kan/db/repository/cardAttachment.repo";
import { generateUID } from "@kan/shared/utils";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { createS3Client } from "@kan/shared/utils";
import { assertPermission } from "@kan/api/utils/permissions";
const MAX_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
export const config = {
api: {
bodyParser: false,
},
};
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user, db } = await createNextApiContext(req);
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
const bucket = env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
if (!bucket) {
return res.status(500).json({ error: "Attachments bucket not configured" });
}
const cardPublicId = req.query.cardPublicId;
if (typeof cardPublicId !== "string" || cardPublicId.length < 12) {
return res.status(400).json({ error: "Invalid cardPublicId" });
}
const contentType = req.headers["content-type"];
const contentLengthHeader = req.headers["content-length"];
const contentLength = contentLengthHeader
? Number.parseInt(contentLengthHeader, 10)
: NaN;
if (typeof contentType !== "string") {
return res.status(400).json({ error: "Missing content type" });
}
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return res.status(400).json({ error: "Missing or invalid content length" });
}
if (contentLength > MAX_SIZE_BYTES) {
return res.status(400).json({ error: "File too large" });
}
const originalFilenameHeader =
(req.headers["x-original-filename"] as string | undefined) ?? "file";
const sanitizedFilename = originalFilenameHeader
.replace(/[^a-zA-Z0-9._-]/g, "_")
.substring(0, 200);
// Get card and check permissions
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
db,
cardPublicId,
);
if (!card) {
return res.status(404).json({ error: "Card not found" });
}
// Check if user has permission to edit the card
try {
await assertPermission(db, user.id, card.workspaceId, "card:edit");
} catch {
return res.status(403).json({ error: "Permission denied" });
}
const s3Key = `${card.workspaceId}/${cardPublicId}/${generateUID()}-${sanitizedFilename}`;
const client = createS3Client();
// Upload the file to S3
await client.send(
new PutObjectCommand({
Bucket: bucket,
Key: s3Key,
Body: req,
ContentType: contentType,
ContentLength: contentLength,
}),
);
// Create attachment record and log activity
const attachment = await cardAttachmentRepo.create(db, {
cardId: card.id,
filename: sanitizedFilename,
originalFilename: originalFilenameHeader,
contentType,
size: contentLength,
s3Key,
createdBy: user.id,
});
await cardActivityRepo.create(db, {
type: "card.updated.attachment.added",
cardId: card.id,
createdBy: user.id,
});
return res.status(200).json({ attachment });
} catch (error) {
console.error("Attachment upload failed", error);
return res.status(500).json({ error: "Internal server error" });
}
},
);

View File

@@ -0,0 +1,101 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { createNextApiContext } from "@kan/api/trpc";
import * as userRepo from "@kan/db/repository/user.repo";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { createS3Client } from "@kan/shared/utils";
const MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
const allowedContentTypes = ["image/jpeg", "image/png", "image/webp"];
export const config = {
api: {
bodyParser: false,
},
};
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user, db } = await createNextApiContext(req);
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
const bucket = env.NEXT_PUBLIC_AVATAR_BUCKET_NAME;
if (!bucket) {
return res.status(500).json({ error: "Avatar bucket not configured" });
}
const contentType = req.headers["content-type"];
const contentLengthHeader = req.headers["content-length"];
const contentLength = contentLengthHeader
? Number.parseInt(contentLengthHeader, 10)
: NaN;
if (typeof contentType !== "string") {
return res.status(400).json({ error: "Missing content type" });
}
if (!allowedContentTypes.includes(contentType)) {
return res.status(400).json({ error: "Invalid content type" });
}
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return res.status(400).json({ error: "Missing or invalid content length" });
}
if (contentLength > MAX_SIZE_BYTES) {
return res.status(400).json({ error: "File too large" });
}
const originalFilenameHeader =
(req.headers["x-original-filename"] as string | undefined) ?? "file";
const sanitizedFilename = originalFilenameHeader
.replace(/[^a-zA-Z0-9._-]/g, "_")
.substring(0, 200);
const s3Key = `${user.id}/${sanitizedFilename}`;
const client = createS3Client();
// Upload the file to S3
await client.send(
new PutObjectCommand({
Bucket: bucket,
Key: s3Key,
Body: req,
ContentType: contentType,
ContentLength: contentLength,
}),
);
// Update user image in database
const updatedUser = await userRepo.update(db, user.id, {
image: s3Key,
});
return res.status(200).json({
key: s3Key,
filename: sanitizedFilename,
contentType,
size: contentLength,
user: updatedUser,
});
} catch (error) {
console.error("Avatar upload failed", error);
return res.status(500).json({ error: "Internal server error" });
}
},
);

View File

@@ -1,75 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { env as nextRuntimeEnv } from "next-runtime-env";
import { createNextApiContext } from "@kan/api/trpc";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
const allowedContentTypes = ["image/jpeg", "image/png"];
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user } = await createNextApiContext(req);
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
const { filename, contentType } = req.body as {
filename: string;
contentType: string;
};
// Specific to avatar uploads for now
const filenameRegex = /^[a-f0-9\-]+\/[a-zA-Z0-9_\-]+(\.jpg|\.jpeg|\.png)$/;
if (!filenameRegex.test(filename)) {
return res.status(400).json({ error: "Invalid filename" });
}
if (
typeof contentType !== "string" ||
!allowedContentTypes.includes(contentType)
) {
return res.status(400).json({ error: "Invalid content type" });
}
const credentials =
env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY
? {
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
}
: undefined;
const client = new S3Client({
region: env.S3_REGION ?? "",
endpoint: env.S3_ENDPOINT ?? "",
forcePathStyle: env.S3_FORCE_PATH_STYLE === "true",
credentials,
});
const signedUrl = await getSignedUrl(
client,
new PutObjectCommand({
Bucket: nextRuntimeEnv("NEXT_PUBLIC_AVATAR_BUCKET_NAME") ?? "",
Key: filename,
ACL: "public-read",
}),
);
return res.status(200).json({ url: signedUrl, key: filename });
} catch (error) {
return res.status(500).json({ error: (error as Error).message });
}
},
);

View File

@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import React from "react";
import {
Dialog,
DialogBackdrop,
@@ -464,19 +465,37 @@ function FormattedShortcut({ shortcut }: { shortcut: KeyboardShortcut }) {
? stroke.modifiers.map(stringifyModifier)
: [];
modifierStrings.forEach((mod) => {
parts.push(<kbd className={kbdClassName}>{mod}</kbd>);
modifierStrings.forEach((mod, index) => {
parts.push(
<kbd key={`mod-${index}-${mod}`} className={kbdClassName}>
{mod}
</kbd>,
);
});
parts.push(<kbd className={kbdClassName}>{stroke.key.toUpperCase()}</kbd>);
parts.push(
<kbd key={`key-${stroke.key}`} className={kbdClassName}>
{stroke.key.toUpperCase()}
</kbd>,
);
return parts;
};
if (shortcut.type === "SEQUENCE") {
const parts: ReactNode[] = [];
shortcut.strokes.forEach((stroke) => {
parts.push(...formatStroke(stroke));
shortcut.strokes.forEach((stroke, strokeIndex) => {
const strokeParts = formatStroke(stroke);
// Add stroke index to keys to ensure uniqueness across multiple strokes
const keyedParts = strokeParts.map((part, partIndex) => {
if (React.isValidElement(part)) {
return React.cloneElement(part, {
key: `stroke-${strokeIndex}-${part.key || partIndex}`,
});
}
return part;
});
parts.push(...keyedParts);
});
return <span className="flex items-center gap-1 text-[11px]">{parts}</span>;
}

View File

@@ -8,6 +8,8 @@ export async function invalidateCard(
utils: ReturnType<typeof api.useUtils>,
cardPublicId: string,
) {
if (!cardPublicId || cardPublicId.length < 12) return;
await Promise.all([
utils.card.byId.invalidate({ cardPublicId }),
utils.card.getActivities.invalidate({ cardPublicId }),

View File

@@ -1,5 +1,3 @@
import { env } from "next-runtime-env";
export const formatToArray = (
value: string | string[] | undefined,
): string[] => {
@@ -52,14 +50,5 @@ export const getAvatarUrl = (imageOrKey: string | null) => {
return imageOrKey;
}
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
const useVirtualHostedUrls = env("NEXT_PUBLIC_USE_VIRTUAL_HOSTED_URLS");
const storageDomain = env("NEXT_PUBLIC_STORAGE_DOMAIN");
if (useVirtualHostedUrls === "true" && storageDomain) {
return `https://${bucket}.${storageDomain}/${imageOrKey}`;
}
const storageUrl = env("NEXT_PUBLIC_STORAGE_URL");
return `${storageUrl}/${bucket}/${imageOrKey}`;
return "";
};

View File

@@ -1,24 +1,40 @@
import Link from "next/link";
import { Fragment } from "react";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { HiLink } from "react-icons/hi";
import { Tooltip } from "~/components/Tooltip";
import { usePopup } from "~/providers/popup";
const displayBaseUrl =
env("NEXT_PUBLIC_KAN_ENV") === "cloud"
? "kan.bn"
: env("NEXT_PUBLIC_BASE_URL");
const linkBaseUrl = env("NEXT_PUBLIC_BASE_URL");
const pathSeparator = (
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600" />
);
const UpdateBoardSlugButton = ({
handleOnClick,
workspaceSlug,
boardSlug,
boardPublicId,
visibility,
isLoading,
canEdit,
}: {
handleOnClick: () => void;
workspaceSlug: string;
boardSlug: string;
boardPublicId: string;
visibility: "public" | "private";
isLoading: boolean;
canEdit: boolean;
}) => {
if (!isLoading && (!workspaceSlug || !boardSlug)) return <></>;
const { showPopup } = usePopup();
if (isLoading) {
return (
@@ -26,44 +42,55 @@ const UpdateBoardSlugButton = ({
);
}
if (!workspaceSlug || !boardSlug || !boardPublicId) return <></>;
const isPublic = visibility === "public";
const boardUrl = isPublic
? `${linkBaseUrl}/${workspaceSlug}/${boardSlug}`
: `${linkBaseUrl}/boards/${boardPublicId}`;
const pathSegments = isPublic
? [displayBaseUrl, workspaceSlug, boardSlug]
: [displayBaseUrl, "boards", boardPublicId];
return (
<Tooltip
content={!canEdit && !isLoading ? t`You don't have permission` : undefined}
content={!canEdit ? t`You don't have permission` : undefined}
>
<button
onClick={canEdit ? handleOnClick : undefined}
disabled={!canEdit || isLoading}
className="hidden cursor-pointer items-center gap-2 rounded-full border-[1px] bg-light-50 p-1 pl-4 pr-1 text-sm text-light-950 hover:bg-light-100 disabled:cursor-not-allowed disabled:opacity-60 dark:border-dark-600 dark:bg-dark-50 dark:text-dark-900 dark:hover:bg-dark-100 xl:flex"
>
<div className="flex items-center">
<span>
{env("NEXT_PUBLIC_KAN_ENV") === "cloud"
? "kan.bn"
: env("NEXT_PUBLIC_BASE_URL")}
</span>
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600"></div>
<span>{workspaceSlug}</span>
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600"></div>
<span>{boardSlug}</span>
</div>
<Link
href={`${env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/${boardSlug}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.stopPropagation();
if (!canEdit) {
e.preventDefault();
}
}}
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-light-200 dark:hover:bg-dark-200"
>
<HiLink className="h-[13px] w-[13px]" />
</Link>
</button>
<div className="flex items-center">
{pathSegments.map((segment, i) => (
<Fragment key={i}>
{i > 0 && pathSeparator}
<span>{segment}</span>
</Fragment>
))}
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(boardUrl).then(
() =>
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Board URL copied to clipboard`,
}),
).catch(() => undefined);
}}
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-light-200 dark:hover:bg-dark-200"
aria-label={t`Copy board link`}
>
<HiLink className="h-[13px] w-[13px]" />
</button>
</button>
</Tooltip>
);
};
export default UpdateBoardSlugButton;

View File

@@ -156,7 +156,7 @@ export function UpdateBoardSlugForm({
<div className="flex items-center gap-2">
<Button
variant="secondary"
href="/settings?tab=workspace"
href="/settings/workspace"
onClick={closeModal}
>
{t`Edit workspace URL`}

View File

@@ -443,6 +443,8 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
isLoading={isLoading}
workspaceSlug={workspace.slug ?? ""}
boardSlug={boardData?.slug ?? ""}
boardPublicId={boardId ?? ""}
visibility={boardData?.visibility ?? "private"}
canEdit={canEditBoard}
/>
<VisibilityButton

View File

@@ -36,12 +36,21 @@ const truncate = (value: string | null, maxLength = 50) => {
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}` : value;
};
const getUserDisplayName = (
user: { name?: string | null; email?: string | null } | null | undefined,
): string => {
if (user?.name?.trim()) return user.name;
if (user?.email) return user.email;
return t`Member`;
};
const getActivityText = ({
type,
toTitle,
fromList,
toList,
memberName,
memberEmail,
isSelf,
label,
fromTitle,
@@ -54,6 +63,7 @@ const getActivityText = ({
fromList: string | null;
toList: string | null;
memberName: string | null;
memberEmail: string | null;
isSelf: boolean;
label: string | null;
fromTitle?: string | null;
@@ -62,6 +72,7 @@ const getActivityText = ({
dateLocale: DateFnsLocale;
mergedLabels?: string[];
}) => {
const displayName = memberName ?? memberEmail ?? t`Member`;
const TextHighlight = ({ children }: { children: React.ReactNode }) => (
<span className="font-medium text-light-1000 dark:text-dark-1000">
{children}
@@ -139,23 +150,23 @@ const getActivityText = ({
);
}
if (type === "card.updated.member.added" && memberName) {
if (type === "card.updated.member.added" && displayName) {
if (isSelf) return <Trans>self-assigned the card</Trans>;
return (
<Trans>
assigned <TextHighlight>{truncate(memberName)}</TextHighlight> to the
assigned <TextHighlight>{truncate(displayName)}</TextHighlight> to the
card
</Trans>
);
}
if (type === "card.updated.member.removed" && memberName) {
if (type === "card.updated.member.removed" && displayName) {
if (isSelf) return <Trans>unassigned themselves from the card</Trans>;
return (
<Trans>
unassigned <TextHighlight>{truncate(memberName)}</TextHighlight> from
unassigned <TextHighlight>{truncate(displayName)}</TextHighlight> from
the card
</Trans>
);
@@ -352,7 +363,7 @@ const ActivityList = ({
limit: ACTIVITIES_PAGE_SIZE,
},
{
enabled: !!cardPublicId,
enabled: !!cardPublicId && cardPublicId.length >= 12,
},
);
@@ -455,6 +466,7 @@ const ActivityList = ({
fromList: activity.fromList?.name ?? null,
toList: activity.toList?.name ?? null,
memberName: activity.member?.user?.name ?? null,
memberEmail: activity.member?.user?.email ?? null,
isSelf: activity.member?.user?.id === sessionData?.user.id,
label: activity.label?.name ?? null,
fromTitle: activity.fromTitle ?? null,
@@ -508,7 +520,7 @@ const ActivityList = ({
)}
</div>
<p className="text-sm">
<span className="font-medium dark:text-dark-1000">{`${activity.user?.name} `}</span>
<span className="font-medium dark:text-dark-1000">{`${getUserDisplayName(activity.user)} `}</span>
<span className="space-x-1 text-light-900 dark:text-dark-800">
{activityText}
</span>

View File

@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { env } from "next-runtime-env";
import { api } from "~/utils/api";
import { invalidateCard } from "~/utils/cardInvalidation";
@@ -18,62 +19,33 @@ export function AttachmentUpload({ cardPublicId }: { cardPublicId: string }) {
const [isDragging, setIsDragging] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const generateUploadUrl = api.attachment.generateUploadUrl.useMutation();
const confirmAttachment = api.attachment.confirm.useMutation({
onSuccess: async () => {
const uploadFile = async (file: File) => {
setUploading(true);
try {
const baseUrl = env("NEXT_PUBLIC_BASE_URL") ?? "";
const response = await fetch(
`${baseUrl}/api/upload/attachment?cardPublicId=${encodeURIComponent(cardPublicId)}`,
{
method: "POST",
headers: {
"Content-Type": file.type,
"x-original-filename": file.name,
},
body: file,
},
);
if (!response.ok) {
throw new Error("Upload failed");
}
await invalidateCard(utils, cardPublicId);
showPopup({
header: t`Attachment uploaded`,
message: t`Your file has been uploaded successfully.`,
icon: "success",
});
},
onError: () => {
showPopup({
header: t`Upload failed`,
message: t`Failed to upload attachment. Please try again.`,
icon: "error",
});
},
onSettled: () => {
setUploading(false);
},
});
const uploadFile = async (file: File) => {
setUploading(true);
try {
// Generate presigned URL
const { url, key } = await generateUploadUrl.mutateAsync({
cardPublicId,
filename: file.name,
contentType: file.type,
size: file.size,
});
// Upload file to S3
const uploadResponse = await fetch(url, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type,
},
});
if (!uploadResponse.ok) {
throw new Error("Upload failed");
}
// Confirm attachment in database
await confirmAttachment.mutateAsync({
cardPublicId,
s3Key: key,
filename: file.name,
originalFilename: file.name,
contentType: file.type,
size: file.size,
});
} catch {
showPopup({
header: t`Upload failed`,

View File

@@ -1,12 +1,13 @@
import { t } from "@lingui/core/macro";
import { formatDistanceToNow } from "date-fns";
import { useState } from "react";
import ContentEditable from "react-contenteditable";
import { useForm } from "react-hook-form";
import { HiEllipsisHorizontal, HiPencil, HiTrash } from "react-icons/hi2";
import Avatar from "~/components/Avatar";
import Button from "~/components/Button";
import Editor from "~/components/Editor";
import type { WorkspaceMember } from "~/components/Editor";
import Dropdown from "~/components/Dropdown";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal";
@@ -29,7 +30,6 @@ const Comment = ({
createdAt,
comment,
isAuthor,
isAdmin,
isEdited = false,
isViewOnly = false,
}: {
@@ -42,7 +42,6 @@ const Comment = ({
createdAt: string;
comment: string | undefined;
isAuthor: boolean;
isAdmin: boolean;
isEdited: boolean;
isViewOnly: boolean;
}) => {
@@ -57,6 +56,30 @@ const Comment = ({
},
});
const { data: cardData } = api.card.byId.useQuery(
{
cardPublicId,
},
{
enabled: !!cardPublicId && cardPublicId.length >= 12,
},
);
const workspaceMembers: WorkspaceMember[] =
cardData?.list.board.workspace.members
.filter((member) => member.email)
.map((member) => ({
publicId: member.publicId,
email: member.email,
user: member.user
? {
id: member.user.id,
name: member.user.name ?? null,
image: member.user.image ?? null,
}
: null,
})) ?? [];
if (!publicId) return null;
const updateCommentMutation = api.card.updateComment.useMutation({
@@ -142,21 +165,28 @@ const Comment = ({
)}
</div>
{!isEditing ? (
<ContentEditable
html={comment ?? ""}
disabled={true}
className="break-anywhere mt-2 text-sm"
/>
<div className="mt-2">
<Editor
content={comment ?? null}
readOnly={true}
workspaceMembers={workspaceMembers}
enableYouTubeEmbed={false}
disableHeadings={true}
/>
</div>
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<ContentEditable
placeholder={t`Add a comment...`}
html={watch("comment")}
disabled={false}
onChange={(e) => setValue("comment", e.target.value)}
className="block w-full max-w-[800px] border-0 bg-transparent py-1.5 text-sm text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
/>
<div className="flex justify-end space-x-2">
<div className="mt-2">
<Editor
content={watch("comment")}
onChange={(value) => setValue("comment", value)}
workspaceMembers={workspaceMembers}
enableYouTubeEmbed={false}
placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`}
disableHeadings={true}
/>
</div>
<div className="flex justify-end space-x-2 mt-2">
<Button
size="sm"
variant="ghost"

View File

@@ -1,6 +1,7 @@
import { t } from "@lingui/core/macro";
import {
HiEllipsisHorizontal,
HiLink,
HiOutlineCheckCircle,
HiOutlineTrash,
} from "react-icons/hi2";
@@ -10,18 +11,54 @@ import { authClient } from "@kan/auth/client";
import Dropdown from "~/components/Dropdown";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
export default function CardDropdown({
cardPublicId,
isTemplate,
boardPublicId,
cardCreatedBy,
}: {
cardPublicId: string;
isTemplate?: boolean;
boardPublicId?: string;
cardCreatedBy?: string | null;
}) {
const { openModal } = useModal();
const { showPopup } = usePopup();
const { canEditCard, canDeleteCard } = usePermissions();
const { data: session } = authClient.useSession();
const isCreator = cardCreatedBy && session?.user.id === cardCreatedBy;
const handleCopyCardLink = async () => {
const path =
isTemplate && boardPublicId
? `/templates/${boardPublicId}/cards/${cardPublicId}`
: `/cards/${cardPublicId}`;
const url = `${window.location.origin}${path}`;
try {
await navigator.clipboard.writeText(url);
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Card URL copied to clipboard`,
});
} catch (error) {
console.error(error);
showPopup({
header: t`Unable to copy link`,
icon: "error",
message: t`Please try again.`,
});
}
};
const items = [
{
label: t`Copy card link`,
action: handleCopyCardLink,
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
},
...(canEditCard
? [
{

View File

@@ -1,8 +1,9 @@
import { t } from "@lingui/core/macro";
import ContentEditable from "react-contenteditable";
import { useForm } from "react-hook-form";
import { HiOutlineArrowUp } from "react-icons/hi2";
import Editor from "~/components/Editor";
import type { WorkspaceMember } from "~/components/Editor";
import LoadingSpinner from "~/components/LoadingSpinner";
import { usePermissions } from "~/hooks/usePermissions";
import { usePopup } from "~/providers/popup";
@@ -13,7 +14,13 @@ interface FormValues {
comment: string;
}
const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
const NewCommentForm = ({
cardPublicId,
workspaceMembers,
}: {
cardPublicId: string;
workspaceMembers: WorkspaceMember[];
}) => {
const utils = api.useUtils();
const { showPopup } = usePopup();
const { canCreateComment } = usePermissions();
@@ -23,10 +30,6 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
},
});
const queryParams = {
cardPublicId,
};
const addCommentMutation = api.card.addComment.useMutation({
onError: (_error, _newList) => {
showPopup({
@@ -57,18 +60,13 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
onSubmit={handleSubmit(onSubmit)}
className="flex w-full max-w-[800px] flex-col rounded-xl border border-light-600 bg-light-100 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
>
<ContentEditable
placeholder={t`Add a comment...`}
html={watch("comment")}
disabled={false}
onChange={(e) => setValue("comment", e.target.value)}
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
onKeyDown={async (e) => {
if (e.key === "Enter" && e.shiftKey) {
e.preventDefault();
await handleSubmit(onSubmit)();
}
}}
<Editor
content={watch("comment")}
onChange={(value) => setValue("comment", value)}
workspaceMembers={workspaceMembers}
enableYouTubeEmbed={false}
placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`}
disableHeadings={true}
/>
<div className="flex justify-end">
<button

View File

@@ -5,6 +5,8 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { IoChevronForwardSharp } from "react-icons/io5";
import { authClient } from "@kan/auth/client";
import Avatar from "~/components/Avatar";
import Editor from "~/components/Editor";
import FeedbackModal from "~/components/FeedbackModal";
@@ -14,8 +16,6 @@ import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { EditYouTubeModal } from "~/components/YouTubeEmbed/EditYouTubeModal";
import { authClient } from "@kan/auth/client";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
@@ -53,9 +53,10 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
? router.query.cardId[0]
: router.query.cardId;
const { data: card } = api.card.byId.useQuery({
cardPublicId: cardId ?? "",
});
const { data: card } = api.card.byId.useQuery(
{ cardPublicId: cardId ?? "" },
{ enabled: !!cardId && cardId.length >= 12 },
);
const isCreator = card?.createdBy && session?.user.id === card.createdBy;
const canEdit = canEditCard || isCreator;
@@ -166,7 +167,6 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
const {
modalContentType,
entityId,
openModal,
getModalState,
clearModalState,
isOpen,
@@ -184,9 +184,10 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
? router.query.cardId[0]
: router.query.cardId;
const { data: card, isLoading } = api.card.byId.useQuery({
cardPublicId: cardId ?? "",
});
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardId ?? "" },
{ enabled: !!cardId && cardId.length >= 12 },
);
const isCreator = card?.createdBy && session?.user.id === card.createdBy;
const canEdit = canEditCard || isCreator;
@@ -196,8 +197,24 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
};
const board = card?.list.board;
const workspaceMembers = board?.workspace.members;
const boardId = board?.publicId;
const editorWorkspaceMembers =
workspaceMembers
?.filter((member) => member.email)
.map((member) => ({
publicId: member.publicId,
email: member.email,
user: member.user
? {
id: member.user.id,
name: member.user.name ?? null,
image: member.user.image ?? null,
}
: null,
})) ?? [];
const updateCard = api.card.update.useMutation({
onError: () => {
showPopup({
@@ -284,6 +301,7 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
if (!cardId) return <></>;
return (
<>
<PageHead
@@ -315,7 +333,12 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
</Link>
</div>
<div className="flex items-center gap-2">
<Dropdown cardCreatedBy={card?.createdBy} />
<Dropdown
cardPublicId={cardId}
isTemplate={isTemplate}
boardPublicId={boardId}
cardCreatedBy={card?.createdBy}
/>
</div>
</>
)}
@@ -372,9 +395,15 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
<div className="mt-2">
<Editor
content={card.description}
onChange={canEdit ? (e) => setValue("description", e) : undefined}
onBlur={canEdit ? () => handleSubmit(onSubmit)() : undefined}
workspaceMembers={board?.workspace.members ?? []}
onChange={
canEdit
? (e) => setValue("description", e)
: undefined
}
onBlur={
canEdit ? () => handleSubmit(onSubmit)() : undefined
}
workspaceMembers={workspaceMembers ?? []}
readOnly={!canEdit}
/>
</div>
@@ -418,7 +447,10 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
</div>
{!isTemplate && (
<div className="mt-6">
<NewCommentForm cardPublicId={cardId} />
<NewCommentForm
cardPublicId={cardId}
workspaceMembers={editorWorkspaceMembers}
/>
</div>
)}
</div>

View File

@@ -37,7 +37,7 @@ export default function MembersPage() {
const { data, isLoading } = api.workspace.byId.useQuery(
{ workspacePublicId: workspace.publicId },
// { enabled: workspace?.publicId ? true : false },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const { data: session } = authClient.useSession();
@@ -48,9 +48,11 @@ export default function MembersPage() {
const updateRoleMutation = api.member.updateRole.useMutation({
onSuccess: async () => {
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
if (workspace.publicId && workspace.publicId.length >= 12) {
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
}
showPopup({
header: t`Role updated`,
@@ -124,7 +126,6 @@ export default function MembersPage() {
name={memberName ?? ""}
email={memberEmail ?? ""}
imageUrl={memberImage ? getAvatarUrl(memberImage) : undefined}
icon={showPendingIcon ? "?" : undefined}
/>
)}
</div>

View File

@@ -1,12 +1,13 @@
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { useEffect, useRef, useState } from "react";
import { HiXMark } from "react-icons/hi2";
import { HiLink, HiXMark } from "react-icons/hi2";
import Badge from "~/components/Badge";
import Editor from "~/components/Editor";
import LabelIcon from "~/components/LabelIcon";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import ActivityList from "~/views/card/components/ActivityList";
import { AttachmentThumbnails } from "~/views/card/components/AttachmentThumbnails";
@@ -23,16 +24,35 @@ export function CardModal({
}) {
const router = useRouter();
const { closeModal, isOpen } = useModal();
const { showPopup } = usePopup();
const [showFade, setShowFade] = useState(false);
const [showTopFade, setShowTopFade] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const handleCopyCardLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Card URL copied to clipboard`,
});
} catch (error) {
console.error(error);
showPopup({
header: t`Unable to copy link`,
icon: "error",
message: t`Please try again.`,
});
}
};
const { data, isLoading } = api.card.byId.useQuery(
{
cardPublicId: cardPublicId ?? "",
},
{
enabled: isOpen && !!cardPublicId,
enabled: isOpen && !!cardPublicId && cardPublicId.length >= 12,
},
);
@@ -65,33 +85,44 @@ export function CardModal({
<div className="h-full p-8">
<div className="mb-6">
<div className="flex w-full items-center justify-between">
<button
className="absolute right-[2rem] top-[2rem] rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
<div className="absolute right-[2rem] top-[2rem] flex items-center gap-1">
<button
type="button"
onClick={handleCopyCardLink}
className="rounded p-1.5 transition-all hover:bg-light-200 focus:outline-none dark:hover:bg-dark-100"
aria-label="Copy card link"
>
<HiLink className="h-4 w-4 text-light-900 dark:text-dark-900" />
</button>
<button
type="button"
className="rounded p-1.5 transition-all hover:bg-light-200 focus:outline-none dark:hover:bg-dark-100"
onClick={(e) => {
e.preventDefault();
closeModal();
setTimeout(() => {
void router.replace(
{
pathname: router.pathname,
query: {
...router.query,
workspaceSlug,
boardSlug: [boardSlug],
setTimeout(() => {
void router.replace(
{
pathname: router.pathname,
query: {
...router.query,
workspaceSlug: workspaceSlug ?? "",
boardSlug: [boardSlug ?? ""],
},
},
},
undefined,
{ shallow: true },
);
}, 400);
}}
>
<HiXMark
size={18}
className="dark:text-dark-9000 text-light-900"
/>
</button>
undefined,
{ shallow: true },
);
}, 400);
}}
>
<HiXMark
size={18}
className="text-light-900 dark:text-dark-900"
/>
</button>
</div>
{isLoading ? (
<div className="flex space-x-2">
<div className="h-[2.3rem] w-[300px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />

View File

@@ -30,7 +30,7 @@ export default function PublicBoardView() {
const { showPopup } = usePopup();
const [isRouteLoaded, setIsRouteLoaded] = useState(false);
const { openModal } = useModal();
const { ref: scrollRef, onMouseDown } = useDragToScroll({
enabled: true,
direction: "horizontal",
@@ -70,30 +70,35 @@ export default function PublicBoardView() {
},
);
const CopyBoardLink = () => {
return (
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(window.location.href);
} catch (error) {
console.error(error);
}
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Board URL copied to clipboard`,
});
}}
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
aria-label={`Copy board URL`}
>
<HiLink className={`h-4 w-4 text-light-900 dark:text-dark-900`} />
</button>
);
const handleCopyBoardLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Board URL copied to clipboard`,
});
} catch (error) {
console.error(error);
showPopup({
header: t`Unable to copy link`,
icon: "error",
message: t`Please try again.`,
});
}
};
const CopyBoardLink = () => (
<button
type="button"
onClick={handleCopyBoardLink}
className="rounded p-1.5 transition-all hover:bg-light-200 focus:outline-none dark:hover:bg-dark-100"
aria-label="Copy board URL"
>
<HiLink className="h-4 w-4 text-light-900 dark:text-dark-900" />
</button>
);
const pathWithoutQuery = router.asPath.split("?")[0];
const splitPath = pathWithoutQuery?.split("/") ?? [];
const cardPublicId = splitPath.length > 3 ? splitPath[3] : null;

View File

@@ -28,9 +28,11 @@ export default function PermissionsSettings() {
});
// Refresh any relevant workspace data
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
if (workspace.publicId && workspace.publicId.length >= 12) {
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
}
},
onError: () => {
showPopup({

View File

@@ -31,9 +31,10 @@ export default function WorkspaceSettings() {
const { data } = api.user.getUser.useQuery();
const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false);
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
const { data: workspaceData } = api.workspace.byId.useQuery(
{ workspacePublicId: workspace.publicId },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const subscriptions = workspaceData?.subscriptions as
| Subscription[]

View File

@@ -58,28 +58,6 @@ export default function Avatar({
const [crop, setCrop] = useState<PercentCrop>();
const imgRef = useRef<HTMLImageElement | null>(null);
const updateUser = api.user.update.useMutation({
onSuccess: async () => {
showPopup({
header: t`Profile image updated`,
message: t`Your profile image has been updated.`,
icon: "success",
});
try {
await utils.user.getUser.refetch();
} catch (e) {
console.error(e);
throw e;
}
},
onError: () => {
showPopup({
header: t`Error updating profile image`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
});
const avatarUrl = userImage ? getAvatarUrl(userImage) : undefined;
@@ -187,29 +165,32 @@ export default function Avatar({
const originalExt = selectedFile.name.split(".").pop() ?? "jpg";
const fileName = `${userId}/avatar-${generateUID()}.${originalExt}`;
const baseUrl = env("NEXT_PUBLIC_BASE_URL") ?? "";
const response = await fetch(
env("NEXT_PUBLIC_BASE_URL") + "/api/upload/image",
`${baseUrl}/api/upload/avatar`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Type": blob.type,
"x-original-filename": fileName,
},
body: JSON.stringify({ filename: fileName, contentType: blob.type }),
body: blob,
},
);
if (!response.ok) throw new Error("Failed to get pre-signed URL");
if (!response.ok) {
throw new Error("Failed to upload profile image");
}
const { url } = (await response.json()) as { url: string };
const uploadResponse = await fetch(url, {
method: "PUT",
body: blob,
// User image is updated in the backend, refresh user data
await utils.user.getUser.refetch();
showPopup({
header: t`Profile image updated`,
message: t`Your profile image has been updated.`,
icon: "success",
});
if (!uploadResponse.ok) throw new Error("Failed to upload profile image");
updateUser.mutate({ image: fileName });
setCropDialogOpen(false);
resetCropState();
} catch (error) {
@@ -227,7 +208,7 @@ export default function Avatar({
resetCropState,
selectedFile,
showPopup,
updateUser,
utils.user.getUser,
userId,
]);

View File

@@ -22,9 +22,11 @@ export default function UpdateWorkspaceEmailVisibilityForm({
const updateWorkspace = api.workspace.update.useMutation({
onSuccess: () => {
void utils.workspace.byId.invalidate({
workspacePublicId,
});
if (workspacePublicId && workspacePublicId.length >= 12) {
void utils.workspace.byId.invalidate({
workspacePublicId,
});
}
},
});

View File

@@ -27,6 +27,10 @@
"./utils/rateLimit": {
"types": "./dist/utils/rateLimit.d.ts",
"default": "./src/utils/rateLimit.ts"
},
"./utils/permissions": {
"types": "./dist/utils/permissions.d.ts",
"default": "./src/utils/permissions.ts"
}
},
"license": "GPL-3.0",
@@ -39,8 +43,6 @@
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.802.0",
"@aws-sdk/s3-request-presigner": "^3.812.0",
"@kan/auth": "workspace:*",
"@kan/db": "workspace:*",
"@kan/email": "workspace:^",

View File

@@ -9,7 +9,7 @@ import { generateUID } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { assertPermission } from "../utils/permissions";
import { deleteObject, generateUploadUrl } from "../utils/s3";
import { deleteObject, generateUploadUrl } from "@kan/shared/utils";
export const attachmentRouter = createTRPCRouter({
generateUploadUrl: protectedProcedure

View File

@@ -10,6 +10,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { colours } from "@kan/shared/constants";
import {
convertDueDateFiltersToRanges,
generateAvatarUrl,
generateSlug,
generateUID,
} from "@kan/shared/utils";
@@ -142,7 +143,40 @@ export const boardRouter = createTRPCRouter({
},
);
return result;
if (!result) {
throw new TRPCError({
message: `Board with public ID ${input.boardPublicId} not found`,
code: "NOT_FOUND",
});
}
// Generate presigned URLs for workspace member avatars
const workspaceWithAvatarUrls = result.workspace
? {
...result.workspace,
members: await Promise.all(
result.workspace.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
),
}
: result.workspace;
return {
...result,
workspace: workspaceWithAvatarUrls,
};
}),
bySlug: publicProcedure
.meta({

View File

@@ -10,8 +10,9 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { mergeActivities } from "../utils/activities";
import { sendMentionEmails } from "../utils/notifications";
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
import { generateDownloadUrl } from "../utils/s3";
import { generateAttachmentUrl, generateAvatarUrl } from "@kan/shared/utils";
export const cardRouter = createTRPCRouter({
create: protectedProcedure
@@ -153,6 +154,17 @@ export const cardRouter = createTRPCRouter({
await cardActivityRepo.bulkCreate(ctx.db, cardActivitesInsert);
}
if (input.description) {
sendMentionEmails({
db: ctx.db,
cardPublicId: newCard.publicId,
commentHtml: input.description,
commenterUserId: userId,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
}
return newCard;
}),
addComment: protectedProcedure
@@ -215,6 +227,16 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
sendMentionEmails({
db: ctx.db,
cardPublicId: input.cardPublicId,
commentHtml: input.comment,
commenterUserId: userId,
commentId: newComment.id,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
return newComment;
}),
updateComment: protectedProcedure
@@ -295,6 +317,16 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
sendMentionEmails({
db: ctx.db,
cardPublicId: input.cardPublicId,
commentHtml: input.comment,
commenterUserId: userId,
commentId: updatedComment.id,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
return updatedComment;
}),
deleteComment: protectedProcedure
@@ -631,45 +663,54 @@ export const cardRouter = createTRPCRouter({
});
// Generate URLs for all attachments
const bucket = process.env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
if (result.attachments && Array.isArray(result.attachments)) {
const attachments = result.attachments as {
publicId: string;
contentType: string;
s3Key: string;
originalFilename: string | null;
size?: number | null;
}[];
const attachmentsWithUrls = await Promise.all(
result.attachments.map(async (attachment) => {
const url = await generateAttachmentUrl(attachment.s3Key);
return {
publicId: attachment.publicId,
contentType: attachment.contentType,
s3Key: attachment.s3Key,
originalFilename: attachment.originalFilename,
size: attachment.size,
url,
};
}),
);
const attachmentsWithUrls = await Promise.all(
attachments.map(async (attachment) => {
const base = {
publicId: attachment.publicId,
contentType: attachment.contentType,
s3Key: attachment.s3Key,
originalFilename: attachment.originalFilename,
size: attachment.size,
};
if (!bucket || !attachment.s3Key) {
return { ...base, url: null };
}
try {
const url = await generateDownloadUrl(
bucket,
attachment.s3Key,
86400, // 24 hours expiration
);
return { ...base, url };
} catch {
// If URL generation fails, return attachment with url: null
return { ...base, url: null };
}
}),
);
return { ...result, attachments: attachmentsWithUrls };
}
// Generate presigned URLs for workspace member avatars
const workspaceWithAvatarUrls = result.list.board.workspace
? {
...result.list.board.workspace,
members: await Promise.all(
result.list.board.workspace.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
return { ...result, attachments: [] };
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
),
}
: result.list.board.workspace;
return {
...result,
attachments: attachmentsWithUrls,
list: {
...result.list,
board: {
...result.list.board,
workspace: workspaceWithAvatarUrls,
},
},
};
}),
getActivities: publicProcedure
.meta({
@@ -738,7 +779,39 @@ export const cardRouter = createTRPCRouter({
},
);
const mergedActivities = mergeActivities(result.activities);
// Generate presigned URLs for user avatars in activities
const activitiesWithAvatarUrls = await Promise.all(
result.activities.map(async (activity) => {
const updatedActivity = { ...activity };
// Generate presigned URL for activity user avatar
if (activity.user?.image) {
const userAvatarUrl = await generateAvatarUrl(activity.user.image);
updatedActivity.user = {
...activity.user,
image: userAvatarUrl,
};
}
// Generate presigned URL for member user avatar (if exists)
if (activity.member?.user?.image) {
const memberAvatarUrl = await generateAvatarUrl(
activity.member.user.image,
);
updatedActivity.member = {
...activity.member,
user: {
...activity.member.user,
image: memberAvatarUrl,
},
};
}
return updatedActivity;
}),
);
const mergedActivities = mergeActivities(activitiesWithAvatarUrls);
return {
activities: mergedActivities,
@@ -883,6 +956,15 @@ export const cardRouter = createTRPCRouter({
fromDescription: existingCard.description ?? undefined,
toDescription: input.description,
});
sendMentionEmails({
db: ctx.db,
cardPublicId: input.cardPublicId,
commentHtml: input.description,
commenterUserId: userId,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
}
if (

View File

@@ -24,7 +24,7 @@ import {
createTRPCRouter,
publicProcedure,
} from "../trpc";
import { createS3Client } from "../utils/s3";
import { createS3Client } from "@kan/shared/utils";
const checkDatabaseConnection = async (db: dbClient) => {
try {

View File

@@ -4,6 +4,7 @@ import { z } from "zod";
import * as userRepo from "@kan/db/repository/user.repo";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { generateAvatarUrl } from "@kan/shared/utils";
export const userRouter = createTRPCRouter({
getUser: protectedProcedure
@@ -55,8 +56,12 @@ export const userRouter = createTRPCRouter({
const apiKey = result.apiKeys[0];
// Generate presigned URL for avatar
const imageUrl = await generateAvatarUrl(result.image);
return {
...result,
image: imageUrl,
apiKey: apiKey ?? null,
};
}),
@@ -102,6 +107,12 @@ export const userRouter = createTRPCRouter({
});
}
return result;
// Generate presigned URL for avatar
const imageUrl = await generateAvatarUrl(result.image);
return {
...result,
image: imageUrl,
};
}),
});

View File

@@ -8,6 +8,7 @@ import { generateUID } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { assertPermission } from "../utils/permissions";
import { generateAvatarUrl } from "@kan/shared/utils";
export const workspaceRouter = createTRPCRouter({
all: protectedProcedure
@@ -86,9 +87,27 @@ export const workspaceRouter = createTRPCRouter({
const shouldShowEmails =
isAdmin || result.showEmailsToMembers === true;
// Generate presigned URLs for member avatars
const membersWithAvatarUrls = await Promise.all(
result.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
);
// If emails should be hidden, filter them out
if (!shouldShowEmails) {
const sanitizedMembers = result.members.map((member) => {
const sanitizedMembers = membersWithAvatarUrls.map((member) => {
// If user doesn't have a display name, use anonymous identifier
const displayName =
member.user?.name?.trim() ?? `anonymous_${member.publicId}`;
@@ -120,7 +139,10 @@ export const workspaceRouter = createTRPCRouter({
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
}
return result;
return {
...result,
members: membersWithAvatarUrls,
};
}),
bySlug: publicProcedure
.meta({

View File

@@ -0,0 +1,133 @@
import { env } from "next-runtime-env";
import type { dbClient } from "@kan/db/client";
import * as cardRepo from "@kan/db/repository/card.repo";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as notificationRepo from "@kan/db/repository/notification.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { sendEmail } from "@kan/email";
import { parseMentionsFromHTML } from "@kan/shared/utils";
/**
* Sends mention notification emails to mentioned members
* Only sends emails for new mentions (checks notification table to avoid duplicates)
*/
export async function sendMentionEmails({
db,
cardPublicId,
commentHtml,
commenterUserId,
commentId,
}: {
db: dbClient;
cardPublicId: string;
commentHtml: string;
commenterUserId: string;
commentId?: number;
}) {
try {
// Parse mentions from HTML
const mentionPublicIds = parseMentionsFromHTML(commentHtml);
if (mentionPublicIds.length === 0) return;
// Get card with board information
const card = await cardRepo.getWithListAndMembersByPublicId(db, cardPublicId);
if (!card?.list.board) return;
const board = card.list.board;
const boardName = board.name;
const cardTitle = card.title;
const cardId = card.id;
// Get workspace ID from workspace publicId
const workspace = await workspaceRepo.getByPublicId(
db,
board.workspace.publicId,
);
if (!workspace?.id) return;
const workspaceId = workspace.id;
// Get commenter information
const commenter = await userRepo.getById(db, commenterUserId);
if (!commenter) return;
const commenterName = commenter.name ?? commenter.email;
// Get mentioned members with full details (filtered by workspace)
const membersWithDetails = await memberRepo.getByPublicIdsWithUsers(
db,
mentionPublicIds,
workspaceId,
);
// Filter out the commenter
const membersToNotify = membersWithDetails.filter(
(member) => member.user?.id !== commenterUserId,
);
if (membersToNotify.length === 0) return;
const baseUrl = env("NEXT_PUBLIC_BASE_URL");
const cardUrl = `${baseUrl}/cards/${cardPublicId}`;
// Send emails to all mentioned members (only if notification doesn't exist)
await Promise.all(
membersToNotify.map(async (member) => {
const userId = member.user?.id;
const email = member.user?.email ?? member.email;
// Skip pending members (no userId) - they can be mentioned but won't receive emails
if (!userId || !email) return;
try {
// Check if notification already exists for this mention
const notificationExists = await notificationRepo.exists(db, {
userId,
cardId,
type: "mention",
});
// If notification already exists, skip sending email
if (notificationExists) {
return;
}
// Create notification record
await notificationRepo.create(db, {
type: "mention",
userId,
cardId,
commentId,
});
// Send email
await sendEmail(
email,
`${commenterName} mentioned you in a comment on ${cardTitle}`,
"MENTION",
{
commenterName,
boardName,
cardTitle,
cardUrl,
},
);
} catch (error) {
console.error("Failed to send mention email:", {
email,
cardPublicId,
error: error instanceof Error ? error.message : String(error),
});
}
}),
);
} catch (error) {
console.error("Error sending mention emails:", {
cardPublicId,
error: error instanceof Error ? error.message : String(error),
});
}
}

View File

@@ -1,4 +1,4 @@
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { ChatOrPushProviderEnum } from "@novu/api/models/components";
import { createAuthMiddleware } from "better-auth/api";
import { env } from "next-runtime-env";
@@ -7,7 +7,7 @@ import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import { notificationClient } from "@kan/email";
import { createEmailUnsubscribeLink } from "@kan/shared";
import { createEmailUnsubscribeLink, createS3Client } from "@kan/shared";
import { downloadImage } from "./utils";
@@ -61,20 +61,7 @@ export function createDatabaseHooks(db: dbClient) {
!user.image.includes(storageDomain)
) {
try {
const credentials =
env("S3_ACCESS_KEY_ID") && env("S3_SECRET_ACCESS_KEY")
? {
accessKeyId: env("S3_ACCESS_KEY_ID")!,
secretAccessKey: env("S3_SECRET_ACCESS_KEY")!,
}
: undefined;
const client = new S3Client({
region: env("S3_REGION") ?? "",
endpoint: env("S3_ENDPOINT") ?? "",
forcePathStyle: env("S3_FORCE_PATH_STYLE") === "true",
credentials,
});
const client = createS3Client();
const allowedFileExtensions = ["jpg", "jpeg", "png", "webp"];

View File

@@ -0,0 +1,46 @@
CREATE TYPE "public"."notification_type" AS ENUM('mention', 'workspace.member.added', 'workspace.member.removed', 'workspace.role.changed');--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "notification" (
"id" bigserial PRIMARY KEY NOT NULL,
"publicId" varchar(12) NOT NULL,
"type" "notification_type" NOT NULL,
"userId" uuid NOT NULL,
"cardId" bigint,
"commentId" bigint,
"workspaceId" bigint,
"metadata" text,
"readAt" timestamp,
"createdAt" timestamp DEFAULT now() NOT NULL,
"deletedAt" timestamp,
CONSTRAINT "notification_publicId_unique" UNIQUE("publicId")
);
--> statement-breakpoint
ALTER TABLE "notification" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "public"."card"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_commentId_card_comments_id_fk" FOREIGN KEY ("commentId") REFERENCES "public"."card_comments"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_deleted_idx" ON "notification" USING btree ("userId","deletedAt");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_read_deleted_idx" ON "notification" USING btree ("userId","readAt","deletedAt");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_type_card_idx" ON "notification" USING btree ("userId","type","cardId");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_type_workspace_idx" ON "notification" USING btree ("userId","type","workspaceId");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_created_idx" ON "notification" USING btree ("userId","createdAt");

File diff suppressed because it is too large Load Diff

View File

@@ -176,6 +176,13 @@
"when": 1769983198190,
"tag": "20260201215958_AddUserBoardFavouritesTable",
"breakpoints": true
},
{
"idx": 25,
"version": "7",
"when": 1770500457005,
"tag": "20260207214056_AddNotificationsTable",
"breakpoints": true
}
]
}

View File

@@ -212,6 +212,7 @@ export const getByPublicId = async (
columns: {
publicId: true,
email: true,
status: true,
},
with: {
user: {

View File

@@ -420,6 +420,7 @@ export const getWithListAndMembersByPublicId = async (
) => {
const card = await db.query.cards.findFirst({
columns: {
id: true,
publicId: true,
title: true,
description: true,
@@ -507,6 +508,7 @@ export const getWithListAndMembersByPublicId = async (
columns: {
publicId: true,
email: true,
status: true,
},
with: {
user: {

View File

@@ -63,6 +63,36 @@ export const getById = async (db: dbClient, memberId: number) => {
});
};
export const getByPublicIdsWithUsers = async (
db: dbClient,
memberPublicIds: string[],
workspaceId?: number,
) => {
return db.query.workspaceMembers.findMany({
where: (members, { inArray: inArrayFn, eq, and, isNull: isNullFn }) => {
const conditions = [inArrayFn(members.publicId, memberPublicIds)];
if (workspaceId) {
conditions.push(eq(members.workspaceId, workspaceId));
}
conditions.push(eq(members.status, "active"));
conditions.push(isNullFn(members.deletedAt));
return and(...conditions);
},
with: {
user: {
columns: {
id: true,
name: true,
email: true,
},
},
},
});
};
export const getByEmailAndStatus = async (
db: dbClient,
email: string,

View File

@@ -0,0 +1,98 @@
import { and, count, eq, isNull } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import type { NotificationType } from "@kan/db/schema";
import { notifications } from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils";
export const create = async (
db: dbClient,
notificationInput: {
type: NotificationType;
userId: string;
cardId?: number;
commentId?: number;
workspaceId?: number;
metadata?: string;
},
) => {
const [result] = await db
.insert(notifications)
.values({
publicId: generateUID(),
type: notificationInput.type,
userId: notificationInput.userId,
cardId: notificationInput.cardId,
commentId: notificationInput.commentId,
workspaceId: notificationInput.workspaceId,
metadata: notificationInput.metadata,
})
.returning();
return result;
};
export const exists = async (
db: dbClient,
args: {
userId: string;
type: NotificationType;
cardId?: number;
workspaceId?: number;
commentId?: number;
},
) => {
const result = await db.query.notifications.findFirst({
where: (notifications, { eq, and, isNull: isNullFn }) => {
const conditions = [
eq(notifications.userId, args.userId),
eq(notifications.type, args.type),
isNullFn(notifications.deletedAt),
];
if (args.cardId) {
conditions.push(eq(notifications.cardId, args.cardId));
}
if (args.workspaceId) {
conditions.push(eq(notifications.workspaceId, args.workspaceId));
}
return and(...conditions);
},
});
return !!result;
};
export const markAsRead = async (
db: dbClient,
notificationId: number,
) => {
const [result] = await db
.update(notifications)
.set({ readAt: new Date() })
.where(eq(notifications.id, notificationId))
.returning();
return result;
};
export const getUnreadCount = async (
db: dbClient,
userId: string,
) => {
const result = await db
.select({ count: count() })
.from(notifications)
.where(
and(
eq(notifications.userId, userId),
isNull(notifications.readAt),
isNull(notifications.deletedAt),
),
);
return result[0]?.count ?? 0;
};

View File

@@ -13,3 +13,4 @@ export * from "./workspaces";
export * from "./subscriptions";
export * from "./workspaceInviteLinks";
export * from "./permissions";
export * from "./notifications";

View File

@@ -0,0 +1,98 @@
import { relations } from "drizzle-orm";
import {
bigint,
bigserial,
index,
pgEnum,
pgTable,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { cards } from "./cards";
import { comments } from "./cards";
import { users } from "./users";
import { workspaces } from "./workspaces";
export const notificationTypes = [
"mention",
"workspace.member.added",
"workspace.member.removed",
"workspace.role.changed",
] as const;
export type NotificationType = (typeof notificationTypes)[number];
export const notificationTypeEnum = pgEnum("notification_type", notificationTypes);
export const notifications = pgTable(
"notification",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
type: notificationTypeEnum("type").notNull(),
userId: uuid("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
cardId: bigint("cardId", { mode: "number" }).references(() => cards.id, {
onDelete: "cascade",
}),
commentId: bigint("commentId", { mode: "number" }).references(
() => comments.id,
{ onDelete: "cascade" },
),
workspaceId: bigint("workspaceId", { mode: "number" }).references(
() => workspaces.id,
{ onDelete: "cascade" },
),
metadata: text("metadata"),
readAt: timestamp("readAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
deletedAt: timestamp("deletedAt"),
},
(table) => [
index("notification_user_deleted_idx").on(table.userId, table.deletedAt),
index("notification_user_read_deleted_idx").on(
table.userId,
table.readAt,
table.deletedAt,
),
index("notification_user_type_card_idx").on(
table.userId,
table.type,
table.cardId,
),
index("notification_user_type_workspace_idx").on(
table.userId,
table.type,
table.workspaceId,
),
index("notification_user_created_idx").on(table.userId, table.createdAt),
],
).enableRLS();
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, {
fields: [notifications.userId],
references: [users.id],
relationName: "notificationsUser",
}),
card: one(cards, {
fields: [notifications.cardId],
references: [cards.id],
relationName: "notificationsCard",
}),
comment: one(comments, {
fields: [notifications.commentId],
references: [comments.id],
relationName: "notificationsComment",
}),
workspace: one(workspaces, {
fields: [notifications.workspaceId],
references: [workspaces.id],
relationName: "notificationsWorkspace",
}),
}));

View File

@@ -3,14 +3,16 @@ import nodemailer from "nodemailer";
import JoinWorkspaceTemplate from "./templates/join-workspace";
import MagicLinkTemplate from "./templates/magic-link";
import MentionTemplate from "./templates/mention";
import ResetPasswordTemplate from "./templates/reset-password";
type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE" | "RESET_PASSWORD";
type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE" | "RESET_PASSWORD" | "MENTION";
const emailTemplates: Record<Templates, React.FC> = {
const emailTemplates: Record<Templates, React.ComponentType<any>> = {
MAGIC_LINK: MagicLinkTemplate,
JOIN_WORKSPACE: JoinWorkspaceTemplate,
RESET_PASSWORD: ResetPasswordTemplate,
MENTION: MentionTemplate,
};
const transporter = nodemailer.createTransport({

View File

@@ -0,0 +1,108 @@
import { Body } from "@react-email/body";
import { Button } from "@react-email/button";
import { Container } from "@react-email/container";
import { Head } from "@react-email/head";
import { Heading } from "@react-email/heading";
import { Hr } from "@react-email/hr";
import { Html } from "@react-email/html";
import { Link } from "@react-email/link";
import { Preview } from "@react-email/preview";
import { Text } from "@react-email/text";
import { env } from "next-runtime-env";
import * as React from "react";
export const MentionTemplate = ({
commenterName,
boardName,
cardTitle,
cardUrl,
}: {
commenterName: string;
boardName: string;
cardTitle: string;
cardUrl: string;
}) => (
<Html>
<Head />
<Preview>
{commenterName} mentioned you in a comment on {cardTitle}
</Preview>
<Body style={{ backgroundColor: "white" }}>
<Container
style={{
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
margin: "auto",
paddingLeft: "0.75rem",
paddingRight: "0.75rem",
}}
>
<Heading
style={{
marginTop: "2.5rem",
marginBottom: "2.5rem",
fontSize: "24px",
fontWeight: "bold",
color: "#232323",
}}
>
kan.bn
</Heading>
<Heading
style={{ fontSize: "24px", fontWeight: "bold", color: "#232323" }}
>
You were mentioned in a comment
</Heading>
<Text
style={{
fontSize: "0.875rem",
marginBottom: "1rem",
color: "#232323",
}}
>
<strong>{commenterName}</strong> mentioned you in a comment on the
card <strong>{cardTitle}</strong> in the board <strong>{boardName}</strong>.
</Text>
<Button
target="_blank"
href={cardUrl}
style={{
marginBottom: "2rem",
borderRadius: "0.375rem",
backgroundColor: "#282828",
paddingLeft: "1.5rem",
paddingRight: "1.5rem",
paddingTop: "1rem",
paddingBottom: "1rem",
fontSize: "0.875rem",
fontWeight: "500",
lineHeight: "1",
color: "white",
}}
>
View Card
</Button>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
</Text>
</Container>
</Body>
</Html>
);
export default MentionTemplate;

View File

@@ -36,6 +36,8 @@
},
"prettier": "@kan/prettier-config",
"dependencies": {
"@aws-sdk/client-s3": "^3.802.0",
"@aws-sdk/s3-request-presigner": "^3.812.0",
"date-fns": "^4.1.0",
"jose": "^6.1.2",
"nanoid": "^5.0.9",

View File

@@ -3,3 +3,5 @@ export * from "./generateSlug";
export * from "./subscriptions";
export * from "./email";
export * from "./dueDateFilters";
export * from "./s3";
export * from "./mentions";

View File

@@ -0,0 +1,22 @@
/**
* Parses mention data-id attributes from HTML content
* Mentions are stored as: <span data-type="mention" data-id="..." data-label="...">@label</span>
* @param htmlContent - The HTML content to parse
* @returns Array of unique mention public IDs
*/
export function parseMentionsFromHTML(htmlContent: string): string[] {
if (!htmlContent) return [];
// Match all mention spans with data-id attributes
const mentionRegex = /<span[^>]*data-type="mention"[^>]*data-id="([^"]+)"[^>]*>/gi;
const matches = Array.from(htmlContent.matchAll(mentionRegex));
// Extract unique mention IDs
const mentionIds = matches
.map((match) => match[1])
.filter((id): id is string => !!id && id.length >= 12);
// Return unique IDs
return Array.from(new Set(mentionIds));
}

View File

@@ -5,6 +5,7 @@ import {
S3Client,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { env } from "next-runtime-env";
export function createS3Client() {
const credentials =
@@ -67,3 +68,60 @@ export async function deleteObject(bucket: string, key: string) {
}),
);
}
/**
* Generate presigned URL for an avatar image
* Returns the URL as-is if it's already a full URL (external provider)
* Returns presigned URL if it's an S3 key
* Returns null if image key is missing, bucket is not configured, or URL generation fails
*/
export async function generateAvatarUrl(
imageKey: string | null | undefined,
expiresIn = 86400, // 24 hours
): Promise<string | null> {
if (!imageKey) {
return null;
}
if (imageKey.startsWith("http://") || imageKey.startsWith("https://")) {
return imageKey;
}
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
if (!bucket) {
return null;
}
try {
return await generateDownloadUrl(bucket, imageKey, expiresIn);
} catch {
// If URL generation fails, return null
return null;
}
}
/**
* Generate presigned URL for an attachment
* Returns null if attachment key is missing, bucket is not configured, or URL generation fails
*/
export async function generateAttachmentUrl(
attachmentKey: string | null | undefined,
expiresIn = 86400, // 24 hours
): Promise<string | null> {
if (!attachmentKey) {
return null;
}
const bucket = env("NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME");
if (!bucket) {
return null;
}
try {
return await generateDownloadUrl(bucket, attachmentKey, expiresIn);
} catch {
// If URL generation fails, return null
return null;
}
}

24
pnpm-lock.yaml generated
View File

@@ -136,6 +136,9 @@ importers:
'@tiptap/extension-placeholder':
specifier: ^2.14.0
version: 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)
'@tiptap/extension-typography':
specifier: ^3.18.0
version: 3.18.0(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))
'@tiptap/pm':
specifier: ^2.14.0
version: 2.26.1
@@ -293,12 +296,6 @@ importers:
packages/api:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.802.0
version: 3.879.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.812.0
version: 3.879.0
'@kan/auth':
specifier: workspace:*
version: link:../auth
@@ -483,6 +480,12 @@ importers:
packages/shared:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.802.0
version: 3.879.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.812.0
version: 3.879.0
date-fns:
specifier: ^4.1.0
version: 4.1.0
@@ -3779,6 +3782,11 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-typography@3.18.0':
resolution: {integrity: sha512-zTNGJjhJG3lObUhhTbDC1IOyi1DCiCd6i11xsnJDPy5BODYc7t7ZP6VMOWU0LIuMsK9kX02dXoNU7OarJgLpCg==}
peerDependencies:
'@tiptap/core': ^3.18.0
'@tiptap/pm@2.26.1':
resolution: {integrity: sha512-8aF+mY/vSHbGFqyG663ds84b+vca5Lge3tHdTMTKazxCnhXR9dn2oQJMnZ78YZvdRbkPkMJJHti9h3K7u2UQvw==}
@@ -11919,6 +11927,10 @@ snapshots:
dependencies:
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
'@tiptap/extension-typography@3.18.0(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))':
dependencies:
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
'@tiptap/pm@2.26.1':
dependencies:
prosemirror-changeset: 2.3.1