Compare commits
11 Commits
fix/openap
...
fix/kan-24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcb28a3429 | ||
|
|
f938723d9a | ||
|
|
0928709173 | ||
|
|
ade9aad4bf | ||
|
|
5fbaf26707 | ||
|
|
b69835f7de | ||
|
|
58ff6ad041 | ||
|
|
adbc945ed6 | ||
|
|
c5d85cdb2d | ||
|
|
56e4c43cfa | ||
|
|
07fc41761b |
@@ -109,6 +109,7 @@ checksums:
|
|||||||
Complete%20control%20and%20ownership%3A/singular: 0d8b682ba873272217425ccfc96aa9cd
|
Complete%20control%20and%20ownership%3A/singular: 0d8b682ba873272217425ccfc96aa9cd
|
||||||
completed%20a%20checklist%20item/singular: 757b04c6c80cc927e1c597c0ad4fda33
|
completed%20a%20checklist%20item/singular: 757b04c6c80cc927e1c597c0ad4fda33
|
||||||
completed%20checklist%20item%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: 71ec18acf051fc909a48a07ca3578673
|
completed%20checklist%20item%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: 71ec18acf051fc909a48a07ca3578673
|
||||||
|
Confirm%20your%20email%20preferences%3A/singular: 043c161dd9866231ae2418fdd9f61b9c
|
||||||
Confirm%20your%20new%20password/singular: a0d2935d7b63f8dd19d7c0de47524416
|
Confirm%20your%20new%20password/singular: a0d2935d7b63f8dd19d7c0de47524416
|
||||||
Connect%20Trello/singular: 4440a0b9e387ef7136e3958e7a089213
|
Connect%20Trello/singular: 4440a0b9e387ef7136e3958e7a089213
|
||||||
Connect%20your%20favorite%20tools%20to%20streamline%20your%20workflow./singular: 033c5dcdb059ccb634aef7fe63f2f45d
|
Connect%20your%20favorite%20tools%20to%20streamline%20your%20workflow./singular: 033c5dcdb059ccb634aef7fe63f2f45d
|
||||||
@@ -167,6 +168,7 @@ checksums:
|
|||||||
Display%20name%20must%20be%20at%20least%203%20characters%20long/singular: 84178b79845a70937af5e26020403052
|
Display%20name%20must%20be%20at%20least%203%20characters%20long/singular: 84178b79845a70937af5e26020403052
|
||||||
Display%20name%20updated/singular: 4e27503ca24ef5f009713951ca9e5b97
|
Display%20name%20updated/singular: 4e27503ca24ef5f009713951ca9e5b97
|
||||||
Do%20you%20offer%20a%20free%20plan%3F/singular: 3ef16b0994f0d9ae17067782ca9bed1b
|
Do%20you%20offer%20a%20free%20plan%3F/singular: 3ef16b0994f0d9ae17067782ca9bed1b
|
||||||
|
Do%20you%20want%20to%20unsubscribe%3F/singular: 8173b82c731ba885939d223eeef2e40b
|
||||||
docs/singular: 307bff5678ada918da1f1ec3edc76cac
|
docs/singular: 307bff5678ada918da1f1ec3edc76cac
|
||||||
Docs/singular: 55fea190a0c0fb32acd3dc986cd3cc91
|
Docs/singular: 55fea190a0c0fb32acd3dc986cd3cc91
|
||||||
Documentation/singular: 1563fcb5ddb5037b0709ccd3dd384a92
|
Documentation/singular: 1563fcb5ddb5037b0709ccd3dd384a92
|
||||||
@@ -508,6 +510,7 @@ checksums:
|
|||||||
Unlimited%20lists/singular: 5c418701b27b9acbc5dd199719007362
|
Unlimited%20lists/singular: 5c418701b27b9acbc5dd199719007362
|
||||||
Unlimited%20members/singular: f2949ca2dc18b0063af92e60779abb65
|
Unlimited%20members/singular: f2949ca2dc18b0063af92e60779abb65
|
||||||
Unlimited%20workspaces/singular: b504d01997bf908af1d486d9942fed36
|
Unlimited%20workspaces/singular: b504d01997bf908af1d486d9942fed36
|
||||||
|
Unsubscribe/singular: d1a0b07295687d1f08d4df1c411072dd
|
||||||
Update/singular: 079fc039262fd31b10532929685c2d1b
|
Update/singular: 079fc039262fd31b10532929685c2d1b
|
||||||
Update%20label/singular: 97880b503dfe956941c5f23025a1c102
|
Update%20label/singular: 97880b503dfe956941c5f23025a1c102
|
||||||
updated%20a%20checklist%20item/singular: 198e1d1d503f4b69b752dd84d0cc0e9d
|
updated%20a%20checklist%20item/singular: 198e1d1d503f4b69b752dd84d0cc0e9d
|
||||||
@@ -533,6 +536,7 @@ checksums:
|
|||||||
View%20workspace/singular: c9e4b789330b08c18ca8db7044409e5f
|
View%20workspace/singular: c9e4b789330b08c18ca8db7044409e5f
|
||||||
Visibility/singular: 1ff412f8fcde0f460acbf11153649646
|
Visibility/singular: 1ff412f8fcde0f460acbf11153649646
|
||||||
We%20are%20using%20the%20%3C0%3EAGPL-3.0%20license%3C%2F0%3E./singular: 08c5ddd938a2a70f61d684e41e18697a
|
We%20are%20using%20the%20%3C0%3EAGPL-3.0%20license%3C%2F0%3E./singular: 08c5ddd938a2a70f61d684e41e18697a
|
||||||
|
We%20couldn't%20update%20your%20preferences.%20Please%20try%20again./singular: 69d1d2575f2e73b08ac622bde0a45b4d
|
||||||
We're%20just%20getting%20started.%20/singular: 2dfa121e0ff1914e08068308a81973c0
|
We're%20just%20getting%20started.%20/singular: 2dfa121e0ff1914e08068308a81973c0
|
||||||
Welcome%20back/singular: 4928884739ba559e6e4b960e80fe1452
|
Welcome%20back/singular: 4928884739ba559e6e4b960e80fe1452
|
||||||
What%20license%20are%20you%20using%3F/singular: 59d00e1bf28edc21b77fec94875f57ca
|
What%20license%20are%20you%20using%3F/singular: 59d00e1bf28edc21b77fec94875f57ca
|
||||||
@@ -565,6 +569,7 @@ checksums:
|
|||||||
You%20can%20self-host%20by%20following%20the%20instructions%20in%20our%20%3C0%3Erepo%3C%2F0%3E./singular: a6152a41b2d8f64d5a657e5b8bc808a9
|
You%20can%20self-host%20by%20following%20the%20instructions%20in%20our%20%3C0%3Erepo%3C%2F0%3E./singular: a6152a41b2d8f64d5a657e5b8bc808a9
|
||||||
You%20have%20been%20logged%20in%20successfully./singular: ef8fad1dce13ae4112f17c5258655fea
|
You%20have%20been%20logged%20in%20successfully./singular: ef8fad1dce13ae4112f17c5258655fea
|
||||||
You%20have%20been%20signed%20up%20successfully./singular: f614a6e3b45f5ffb9a3b0fb420fef84b
|
You%20have%20been%20signed%20up%20successfully./singular: f614a6e3b45f5ffb9a3b0fb420fef84b
|
||||||
|
You%20have%20been%20unsubscribed!/singular: e0b9985faa4e7f25b71acc77750923a6
|
||||||
You%20have%20unlimited%20seats%20with%20your%20Pro%20Plan.%20There%20is%20no%20additional%20charge%20for%20new%20members!/singular: e3dc59a5ba7211cd3d8516b3a79d85ca
|
You%20have%20unlimited%20seats%20with%20your%20Pro%20Plan.%20There%20is%20no%20additional%20charge%20for%20new%20members!/singular: e3dc59a5ba7211cd3d8516b3a79d85ca
|
||||||
You've%20been%20invited%20to%20join%20a%20workspace%20on%20kan.bn./singular: 257b840726f972f384243a72767f880f
|
You've%20been%20invited%20to%20join%20a%20workspace%20on%20kan.bn./singular: 257b840726f972f384243a72767f880f
|
||||||
You've%20been%20invited%20to%20join%20a%20workspace./singular: 24fc6cdc8740f37a83df85f582f03293
|
You've%20been%20invited%20to%20join%20a%20workspace./singular: 24fc6cdc8740f37a83df85f582f03293
|
||||||
@@ -576,6 +581,7 @@ checksums:
|
|||||||
Your%20profile%20image%20has%20been%20updated./singular: 099e1780348cd7bf617721344371a430
|
Your%20profile%20image%20has%20been%20updated./singular: 099e1780348cd7bf617721344371a430
|
||||||
Your%20Trello%20account%20has%20been%20disconnected./singular: 126aeeb73f7e4cad338056e4c933f39b
|
Your%20Trello%20account%20has%20been%20disconnected./singular: 126aeeb73f7e4cad338056e4c933f39b
|
||||||
Your%20Trello%20account%20is%20connected./singular: 0499d193001fce2fbec445ae034ce512
|
Your%20Trello%20account%20is%20connected./singular: 0499d193001fce2fbec445ae034ce512
|
||||||
|
Your%20unsubscribe%20link%20is%20missing%20a%20token.%20Please%20open%20the%20latest%20email%20and%20try%20again./singular: 3e4cce96bc1197a1caa2b7fb4d3c7e53
|
||||||
Your%20workspace%20description%20has%20been%20updated./singular: 98de6f65c567c6654cc0adce3c9d5656
|
Your%20workspace%20description%20has%20been%20updated./singular: 98de6f65c567c6654cc0adce3c9d5656
|
||||||
Your%20workspace%20has%20been%20deleted./singular: e7a3efcfc7dd18cb3e917acb67498292
|
Your%20workspace%20has%20been%20deleted./singular: e7a3efcfc7dd18cb3e917acb67498292
|
||||||
Your%20workspace%20name%20has%20been%20updated./singular: a87ea3b0d71e6dc5dd525d77114a9322
|
Your%20workspace%20name%20has%20been%20updated./singular: a87ea3b0d71e6dc5dd525d77114a9322
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"@lingui/conf": "^5.3.2",
|
"@lingui/conf": "^5.3.2",
|
||||||
"@lingui/macro": "^5.3.2",
|
"@lingui/macro": "^5.3.2",
|
||||||
"@lingui/react": "^5.3.2",
|
"@lingui/react": "^5.3.2",
|
||||||
|
"@novu/api": "^3.11.0",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"aws-sdk": "^2.1692.0",
|
"aws-sdk": "^2.1692.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
|
"jose": "^6.1.2",
|
||||||
"next": "^15.3.4",
|
"next": "^15.3.4",
|
||||||
"next-logger": "^5.0.1",
|
"next-logger": "^5.0.1",
|
||||||
"next-runtime-env": "^1.7.2",
|
"next-runtime-env": "^1.7.2",
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import type { KeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||||
import LottieIcon from "~/components/LottieIcon";
|
import LottieIcon from "~/components/LottieIcon";
|
||||||
import { useIsMobile } from "~/hooks/useMediaQuery";
|
import { useIsMobile } from "~/hooks/useMediaQuery";
|
||||||
|
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||||
|
|
||||||
const Button: React.FC<{
|
const Button: React.FC<{
|
||||||
href: string;
|
href: string;
|
||||||
@@ -12,10 +14,20 @@ const Button: React.FC<{
|
|||||||
json: object;
|
json: object;
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
onCloseSideNav?: () => void;
|
onCloseSideNav?: () => void;
|
||||||
}> = ({ href, current, name, json, isCollapsed = false, onCloseSideNav }) => {
|
keyboardShortcut: KeyboardShortcut;
|
||||||
|
}> = ({
|
||||||
|
href,
|
||||||
|
current,
|
||||||
|
name,
|
||||||
|
json,
|
||||||
|
isCollapsed = false,
|
||||||
|
keyboardShortcut,
|
||||||
|
onCloseSideNav,
|
||||||
|
}) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { keys: shortcutKeys } = useKeyboardShortcut(keyboardShortcut);
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
@@ -34,18 +46,27 @@ const Button: React.FC<{
|
|||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"group flex h-[34px] items-center rounded-md p-1.5 text-sm font-normal leading-6 hover:bg-light-200 hover:text-light-1000 dark:hover:bg-dark-200 dark:hover:text-dark-1000",
|
"group flex h-[34px] items-center justify-between rounded-md p-1.5 text-sm font-normal leading-6 hover:bg-light-200 hover:text-light-1000 dark:hover:bg-dark-200 dark:hover:text-dark-1000",
|
||||||
current
|
current
|
||||||
? "bg-light-200 text-light-1000 dark:bg-dark-200 dark:text-dark-1000"
|
? "bg-light-200 text-light-1000 dark:bg-dark-200 dark:text-dark-1000"
|
||||||
: "text-neutral-600 dark:bg-dark-100 dark:text-dark-900",
|
: "text-neutral-600 dark:bg-dark-100 dark:text-dark-900",
|
||||||
isCollapsed
|
|
||||||
? "justify-start gap-x-3 md:justify-center md:gap-x-0"
|
|
||||||
: "gap-x-3",
|
|
||||||
)}
|
)}
|
||||||
title={isCollapsed ? name : undefined}
|
title={isCollapsed ? name : undefined}
|
||||||
>
|
>
|
||||||
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
<div
|
||||||
<span className={twMerge(isCollapsed && "md:hidden")}>{name}</span>
|
className={twMerge(
|
||||||
|
"flex items-center",
|
||||||
|
isCollapsed
|
||||||
|
? "justify-start gap-x-3 md:justify-center md:gap-x-0"
|
||||||
|
: "gap-x-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
||||||
|
<span className={twMerge(isCollapsed && "md:hidden")}>{name}</span>
|
||||||
|
</div>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="hidden md:group-hover:inline-flex">{shortcutKeys}</div>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import type { Subscription } from "@kan/shared/utils";
|
import type { Subscription } from "@kan/shared/utils";
|
||||||
import { hasActiveSubscription } from "@kan/shared/utils";
|
import { hasActiveSubscription } from "@kan/shared/utils";
|
||||||
|
|
||||||
|
import type { KeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||||
import boardsIconDark from "~/assets/boards-dark.json";
|
import boardsIconDark from "~/assets/boards-dark.json";
|
||||||
import boardsIconLight from "~/assets/boards-light.json";
|
import boardsIconLight from "~/assets/boards-light.json";
|
||||||
import membersIconDark from "~/assets/members-dark.json";
|
import membersIconDark from "~/assets/members-dark.json";
|
||||||
@@ -86,26 +87,59 @@ export default function SideNavigation({
|
|||||||
|
|
||||||
const isDarkMode = resolvedTheme === "dark";
|
const isDarkMode = resolvedTheme === "dark";
|
||||||
|
|
||||||
const navigation = [
|
const navigation: {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: object;
|
||||||
|
keyboardShortcut: KeyboardShortcut;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
name: t`Boards`,
|
name: t`Boards`,
|
||||||
href: "/boards",
|
href: "/boards",
|
||||||
icon: isDarkMode ? boardsIconDark : boardsIconLight,
|
icon: isDarkMode ? boardsIconDark : boardsIconLight,
|
||||||
|
keyboardShortcut: {
|
||||||
|
type: "SEQUENCE",
|
||||||
|
strokes: [{ key: "G" }, { key: "B" }],
|
||||||
|
action: () => router.push("/boards"),
|
||||||
|
group: "NAVIGATION",
|
||||||
|
description: t`Go to boards`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t`Templates`,
|
name: t`Templates`,
|
||||||
href: "/templates",
|
href: "/templates",
|
||||||
icon: isDarkMode ? templatesIconDark : templatesIconLight,
|
icon: isDarkMode ? templatesIconDark : templatesIconLight,
|
||||||
|
keyboardShortcut: {
|
||||||
|
type: "SEQUENCE",
|
||||||
|
strokes: [{ key: "G" }, { key: "T" }],
|
||||||
|
action: () => router.push("/templates"),
|
||||||
|
group: "NAVIGATION",
|
||||||
|
description: t`Go to templates`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t`Members`,
|
name: t`Members`,
|
||||||
href: "/members",
|
href: "/members",
|
||||||
icon: isDarkMode ? membersIconDark : membersIconLight,
|
icon: isDarkMode ? membersIconDark : membersIconLight,
|
||||||
|
keyboardShortcut: {
|
||||||
|
type: "SEQUENCE",
|
||||||
|
strokes: [{ key: "G" }, { key: "M" }],
|
||||||
|
action: () => router.push("/members"),
|
||||||
|
group: "NAVIGATION",
|
||||||
|
description: t`Go to members`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t`Settings`,
|
name: t`Settings`,
|
||||||
href: "/settings",
|
href: "/settings",
|
||||||
icon: isDarkMode ? settingsIconDark : settingsIconLight,
|
icon: isDarkMode ? settingsIconDark : settingsIconLight,
|
||||||
|
keyboardShortcut: {
|
||||||
|
type: "SEQUENCE",
|
||||||
|
strokes: [{ key: "G" }, { key: "S" }],
|
||||||
|
action: () => router.push("/settings"),
|
||||||
|
group: "NAVIGATION",
|
||||||
|
description: t`Go to settings`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -163,6 +197,7 @@ export default function SideNavigation({
|
|||||||
json={item.icon}
|
json={item.icon}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
onCloseSideNav={onCloseSideNav}
|
onCloseSideNav={onCloseSideNav}
|
||||||
|
keyboardShortcut={item.keyboardShortcut}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
51
apps/web/src/components/Tooltip.tsx
Normal file
51
apps/web/src/components/Tooltip.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { Root } from "react-dom/client";
|
||||||
|
import type { Placement } from "tippy.js";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import tippy from "tippy.js";
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
children: ReactNode;
|
||||||
|
content: ReactNode;
|
||||||
|
placement?: Placement;
|
||||||
|
delay?: number | [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tooltip({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
placement = "bottom",
|
||||||
|
delay = [500, 0],
|
||||||
|
}: TooltipProps) {
|
||||||
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const rootRef = useRef<Root | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!triggerRef.current) return;
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
const root = createRoot(container);
|
||||||
|
rootRef.current = root;
|
||||||
|
root.render(content);
|
||||||
|
|
||||||
|
const instance = tippy(triggerRef.current, {
|
||||||
|
content: container,
|
||||||
|
placement,
|
||||||
|
delay,
|
||||||
|
interactive: false,
|
||||||
|
theme: "tooltip",
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
instance.destroy();
|
||||||
|
rootRef.current?.unmount();
|
||||||
|
};
|
||||||
|
}, [content, placement, delay]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={triggerRef} className="inline-flex">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { authClient } from "@kan/auth/client";
|
import { authClient } from "@kan/auth/client";
|
||||||
|
|
||||||
import { useIsMobile } from "~/hooks/useMediaQuery";
|
import { useIsMobile } from "~/hooks/useMediaQuery";
|
||||||
|
import { useKeyboardShortcuts } from "~/providers/keyboard-shortcuts";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { getAvatarUrl } from "~/utils/helpers";
|
import { getAvatarUrl } from "~/utils/helpers";
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export default function UserMenu({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
|
const { openLegend } = useKeyboardShortcuts();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
@@ -169,6 +171,19 @@ export default function UserMenu({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="light-border-600 border-t-[1px] p-1 dark:border-dark-600">
|
<div className="light-border-600 border-t-[1px] p-1 dark:border-dark-600">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (onCloseSideNav && isMobile) {
|
||||||
|
onCloseSideNav();
|
||||||
|
}
|
||||||
|
openLegend();
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
|
>
|
||||||
|
{t`Shortcuts`}
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Link
|
<Link
|
||||||
href="mailto:support@kan.bn"
|
href="mailto:support@kan.bn"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Button, Menu, Transition } from "@headlessui/react";
|
import { Button, Menu, Transition } from "@headlessui/react";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { HiCheck, HiMagnifyingGlass } from "react-icons/hi2";
|
import { HiCheck, HiMagnifyingGlass } from "react-icons/hi2";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import CommandPallette from "./CommandPallette";
|
import CommandPallette from "./CommandPallette";
|
||||||
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
export default function WorkspaceMenu({
|
export default function WorkspaceMenu({
|
||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
@@ -18,17 +20,17 @@ export default function WorkspaceMenu({
|
|||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const { tooltipContent: commandPaletteShortcutTooltipContent } =
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
useKeyboardShortcut({
|
||||||
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
|
type: "PRESS",
|
||||||
event.preventDefault();
|
stroke: {
|
||||||
setIsOpen(true);
|
key: "k",
|
||||||
}
|
modifiers: ["META"],
|
||||||
};
|
},
|
||||||
|
action: () => setIsOpen(true),
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
description: t`Open command menu`,
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
group: "GENERAL",
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -84,15 +86,17 @@ export default function WorkspaceMenu({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Button
|
<Tooltip content={commandPaletteShortcutTooltipContent}>
|
||||||
className={twMerge(
|
<Button
|
||||||
"mb-1 h-[34px] w-[34px] flex-shrink-0 rounded-lg bg-light-200 p-2 hover:bg-light-300 focus:outline-none dark:bg-dark-200 dark:hover:bg-dark-300",
|
className={twMerge(
|
||||||
isCollapsed && "md:mb-2 md:h-9 md:w-9",
|
"mb-1 h-[34px] w-[34px] flex-shrink-0 rounded-lg bg-light-200 p-2 hover:bg-light-300 focus:outline-none dark:bg-dark-200 dark:hover:bg-dark-300",
|
||||||
)}
|
isCollapsed && "md:mb-2 md:h-9 md:w-9",
|
||||||
onClick={() => setIsOpen(true)}
|
)}
|
||||||
>
|
onClick={() => setIsOpen(true)}
|
||||||
<HiMagnifyingGlass className="h-4 w-4" aria-hidden="true" />
|
>
|
||||||
</Button>
|
<HiMagnifyingGlass className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const env = createEnv({
|
|||||||
VK_CLIENT_SECRET: z.string().optional(),
|
VK_CLIENT_SECRET: z.string().optional(),
|
||||||
LINKEDIN_CLIENT_ID: z.string().optional(),
|
LINKEDIN_CLIENT_ID: z.string().optional(),
|
||||||
LINKEDIN_CLIENT_SECRET: z.string().optional(),
|
LINKEDIN_CLIENT_SECRET: z.string().optional(),
|
||||||
|
NOVU_API_KEY: z.string().optional(),
|
||||||
|
EMAIL_UNSUBSCRIBE_SECRET: z.string().optional(),
|
||||||
// Generic OIDC Provider
|
// Generic OIDC Provider
|
||||||
OIDC_CLIENT_ID: z.string().optional(),
|
OIDC_CLIENT_ID: z.string().optional(),
|
||||||
OIDC_CLIENT_SECRET: z.string().optional(),
|
OIDC_CLIENT_SECRET: z.string().optional(),
|
||||||
@@ -87,7 +89,7 @@ export const env = createEnv({
|
|||||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||||
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
||||||
NEXT_PUBLIC_USE_STANDALONE_OUTPUT: z.string().optional(),
|
NEXT_PUBLIC_USE_STANDALONE_OUTPUT: z.string().optional(),
|
||||||
NEXT_PUBLIC_BASE_URL: z.string().url(),
|
NEXT_PUBLIC_BASE_URL: z.string().url().optional(),
|
||||||
NEXT_PUBLIC_STORAGE_URL: z.string().url().optional(),
|
NEXT_PUBLIC_STORAGE_URL: z.string().url().optional(),
|
||||||
NEXT_PUBLIC_AVATAR_BUCKET_NAME: z.string().optional(),
|
NEXT_PUBLIC_AVATAR_BUCKET_NAME: z.string().optional(),
|
||||||
NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME: z.string().optional(),
|
NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME: z.string().optional(),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Konto"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Konto gelöscht"
|
msgstr "Konto gelöscht"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Aktivität"
|
msgstr "Aktivität"
|
||||||
@@ -309,7 +309,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Blogbeitrag"
|
msgstr "Blogbeitrag"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Board"
|
msgstr "Board"
|
||||||
@@ -391,11 +391,12 @@ msgstr "Fehlerbericht"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Abbrechen"
|
msgstr "Abbrechen"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Karte"
|
msgstr "Karte"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Karte nicht gefunden"
|
msgstr "Karte nicht gefunden"
|
||||||
|
|
||||||
@@ -493,6 +494,10 @@ msgstr "hat ein Checklistenelement abgeschlossen"
|
|||||||
msgid "completed checklist item <0>{0}</0>"
|
msgid "completed checklist item <0>{0}</0>"
|
||||||
msgstr "hat Checklistenelement <0>{0}</0> abgeschlossen"
|
msgstr "hat Checklistenelement <0>{0}</0> abgeschlossen"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Bestätigen sie ihre e-mail-einstellungen:"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Bestätigen Sie Ihr neues Passwort"
|
msgstr "Bestätigen Sie Ihr neues Passwort"
|
||||||
@@ -523,12 +528,12 @@ msgstr "Kontaktiere uns"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Content-Erstellung"
|
msgstr "Content-Erstellung"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Fortfahren mit "
|
msgstr "Fortfahren mit "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Fortfahren mit {0}"
|
msgstr "Fortfahren mit {0}"
|
||||||
|
|
||||||
@@ -747,6 +752,10 @@ msgstr "Anzeigename aktualisiert"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "Bietet ihr einen kostenlosen Plan an?"
|
msgstr "Bietet ihr einen kostenlosen Plan an?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "Möchten sie sich abmelden?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "Dokumente"
|
msgstr "Dokumente"
|
||||||
@@ -798,7 +807,7 @@ msgstr "Workspace-URL bearbeiten"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Bearbeitung"
|
msgstr "Bearbeitung"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "E-Mail"
|
msgstr "E-Mail"
|
||||||
|
|
||||||
@@ -818,11 +827,11 @@ msgstr "Geben Sie Ihr aktuelles Passwort ein"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Geben Sie Ihr aktuelles Passwort ein und wählen Sie ein neues sicheres Passwort."
|
msgstr "Geben Sie Ihr aktuelles Passwort ein und wählen Sie ein neues sicheres Passwort."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Gib deine E-Mail-Adresse ein"
|
msgstr "Gib deine E-Mail-Adresse ein"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Gib deinen Namen ein"
|
msgstr "Gib deinen Namen ein"
|
||||||
|
|
||||||
@@ -830,7 +839,7 @@ msgstr "Gib deinen Namen ein"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Geben Sie Ihr neues Passwort ein"
|
msgstr "Geben Sie Ihr neues Passwort ein"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Gib dein Passwort ein"
|
msgstr "Gib dein Passwort ein"
|
||||||
|
|
||||||
@@ -1000,8 +1009,6 @@ msgid "For long-term sustainability, we recognise all good open source projects
|
|||||||
msgstr "Für langfristige Nachhaltigkeit erkennen wir an, dass alle guten Open-Source-Projekte eine Einnahmequelle benötigen. Unsere stammt aus dem kostenpflichtigen Cloud-Angebot für Workspaces mit mehreren Benutzern und für benutzerdefinierte Workspace-Slugs. Es besteht keine Verpflichtung, es zu nutzen, und Sie können die Software kostenlos auf Ihrer eigenen Infrastruktur selbst hosten."
|
msgstr "Für langfristige Nachhaltigkeit erkennen wir an, dass alle guten Open-Source-Projekte eine Einnahmequelle benötigen. Unsere stammt aus dem kostenpflichtigen Cloud-Angebot für Workspaces mit mehreren Benutzern und für benutzerdefinierte Workspace-Slugs. Es besteht keine Verpflichtung, es zu nutzen, und Sie können die Software kostenlos auf Ihrer eigenen Infrastruktur selbst hosten."
|
||||||
|
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
#: src/views/pricing/components/Pricing.tsx:32
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
#: src/views/pricing/components/PricingTiers.tsx:30
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Kostenlos"
|
msgstr "Kostenlos"
|
||||||
@@ -1345,7 +1352,7 @@ msgstr "Von Teams weltweit geliebt"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Niedrige Priorität"
|
msgstr "Niedrige Priorität"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "Magic Link"
|
msgstr "Magic Link"
|
||||||
|
|
||||||
@@ -1476,7 +1483,7 @@ msgstr "Nächste Schritte"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "Keine {0}"
|
msgstr "Keine {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "Derzeit sind keine Authentifizierungsmethoden verfügbar"
|
msgstr "Derzeit sind keine Authentifizierungsmethoden verfügbar"
|
||||||
|
|
||||||
@@ -1524,7 +1531,7 @@ msgstr "Open Source"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Open-Source-Freunde"
|
msgstr "Open-Source-Freunde"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "oder"
|
msgstr "oder"
|
||||||
|
|
||||||
@@ -1606,15 +1613,15 @@ msgstr "Plattform"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Bitte bestätigen Sie Ihr neues Passwort"
|
msgstr "Bitte bestätigen Sie Ihr neues Passwort"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Bitte gib eine gültige E-Mail-Adresse ein"
|
msgstr "Bitte gib eine gültige E-Mail-Adresse ein"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Bitte gib einen gültigen Namen ein"
|
msgstr "Bitte gib einen gültigen Namen ein"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Bitte gib ein gültiges Passwort ein"
|
msgstr "Bitte gib ein gültiges Passwort ein"
|
||||||
|
|
||||||
@@ -1914,7 +1921,7 @@ msgstr "Registrierung deaktiviert"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "Die Registrierung ist derzeit deaktiviert. Bitte versuche es später erneut."
|
msgstr "Die Registrierung ist derzeit deaktiviert. Bitte versuche es später erneut."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "Registrieren mit "
|
msgstr "Registrieren mit "
|
||||||
|
|
||||||
@@ -2264,6 +2271,11 @@ msgstr "Unbegrenzte Mitglieder"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Unbegrenzte Workspaces"
|
msgstr "Unbegrenzte Workspaces"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Abmelden"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2377,6 +2389,10 @@ msgstr "Sichtbarkeit"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "Wir verwenden die <0>AGPL-3.0 lizenz</0>."
|
msgstr "Wir verwenden die <0>AGPL-3.0 lizenz</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "Wir konnten ihre einstellungen nicht aktualisieren. Bitte versuchen sie es erneut."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "Wir stehen erst am anfang. "
|
msgstr "Wir stehen erst am anfang. "
|
||||||
@@ -2515,6 +2531,10 @@ msgstr "Sie haben sich erfolgreich angemeldet."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "Sie haben sich erfolgreich registriert."
|
msgstr "Sie haben sich erfolgreich registriert."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "Sie wurden abgemeldet!"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "Sie haben unbegrenzte Plätze mit Ihrem Pro-Plan. Für neue Mitglieder fallen keine zusätzlichen Kosten an!"
|
msgstr "Sie haben unbegrenzte Plätze mit Ihrem Pro-Plan. Für neue Mitglieder fallen keine zusätzlichen Kosten an!"
|
||||||
@@ -2559,6 +2579,10 @@ msgstr "Dein Trello-Konto wurde getrennt."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Dein Trello-Konto ist verbunden."
|
msgstr "Dein Trello-Konto ist verbunden."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "Ihrem abmeldelink fehlt ein token. Bitte öffnen sie die neueste e-mail und versuchen sie es erneut."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "Deine Workspace-Beschreibung wurde aktualisiert."
|
msgstr "Deine Workspace-Beschreibung wurde aktualisiert."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -91,7 +91,11 @@ msgstr "Account"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Account deleted"
|
msgstr "Account deleted"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/providers/keyboard-shortcuts.tsx:55
|
||||||
|
#~ msgid "Actions"
|
||||||
|
#~ msgstr "Actions"
|
||||||
|
|
||||||
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Activity"
|
msgstr "Activity"
|
||||||
@@ -349,7 +353,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Blog Post"
|
msgstr "Blog Post"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Board"
|
msgstr "Board"
|
||||||
@@ -448,11 +452,12 @@ msgstr "Bug Report"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Cancel"
|
msgstr "Cancel"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Card"
|
msgstr "Card"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Card not found"
|
msgstr "Card not found"
|
||||||
|
|
||||||
@@ -558,6 +563,10 @@ msgstr "completed checklist item <0>{0}</0>"
|
|||||||
#~ msgid "Confirm URL change"
|
#~ msgid "Confirm URL change"
|
||||||
#~ msgstr "Confirm URL change"
|
#~ msgstr "Confirm URL change"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Confirm your email preferences:"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Confirm your new password"
|
msgstr "Confirm your new password"
|
||||||
@@ -588,12 +597,12 @@ msgstr "Contact us"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Content Creation"
|
msgstr "Content Creation"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Continue with "
|
msgstr "Continue with "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Continue with {0}"
|
msgstr "Continue with {0}"
|
||||||
|
|
||||||
@@ -836,6 +845,10 @@ msgstr "Display name updated"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "Do you offer a free plan?"
|
msgstr "Do you offer a free plan?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "Do you want to unsubscribe?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "docs"
|
msgstr "docs"
|
||||||
@@ -887,7 +900,7 @@ msgstr "Edit workspace URL"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Editing"
|
msgstr "Editing"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "email"
|
msgstr "email"
|
||||||
|
|
||||||
@@ -907,11 +920,11 @@ msgstr "Enter your current password"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Enter your current password and choose a new secure password."
|
msgstr "Enter your current password and choose a new secure password."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Enter your email address"
|
msgstr "Enter your email address"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Enter your name"
|
msgstr "Enter your name"
|
||||||
|
|
||||||
@@ -919,7 +932,7 @@ msgstr "Enter your name"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Enter your new password"
|
msgstr "Enter your new password"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Enter your password"
|
msgstr "Enter your password"
|
||||||
|
|
||||||
@@ -1121,6 +1134,10 @@ msgstr "Full-time"
|
|||||||
msgid "Fun"
|
msgid "Fun"
|
||||||
msgstr "Fun"
|
msgstr "Fun"
|
||||||
|
|
||||||
|
#: src/providers/keyboard-shortcuts.tsx:53
|
||||||
|
#~ msgid "General"
|
||||||
|
#~ msgstr "General"
|
||||||
|
|
||||||
#: src/views/auth/signup/index.tsx:69
|
#: src/views/auth/signup/index.tsx:69
|
||||||
#: src/views/home/components/Cta.tsx:61
|
#: src/views/home/components/Cta.tsx:61
|
||||||
#: src/views/home/components/Header.tsx:106
|
#: src/views/home/components/Header.tsx:106
|
||||||
@@ -1178,6 +1195,22 @@ msgstr "Go Home"
|
|||||||
msgid "Go to app"
|
msgid "Go to app"
|
||||||
msgstr "Go to app"
|
msgstr "Go to app"
|
||||||
|
|
||||||
|
#: src/components/SideNavigation.tsx:105
|
||||||
|
#~ msgid "Go to boards"
|
||||||
|
#~ msgstr "Go to boards"
|
||||||
|
|
||||||
|
#: src/components/SideNavigation.tsx:129
|
||||||
|
#~ msgid "Go to members"
|
||||||
|
#~ msgstr "Go to members"
|
||||||
|
|
||||||
|
#: src/components/SideNavigation.tsx:141
|
||||||
|
#~ msgid "Go to settings"
|
||||||
|
#~ msgstr "Go to settings"
|
||||||
|
|
||||||
|
#: src/components/SideNavigation.tsx:117
|
||||||
|
#~ msgid "Go to templates"
|
||||||
|
#~ msgstr "Go to templates"
|
||||||
|
|
||||||
#: src/views/boards/components/TemplateBoards.tsx:18
|
#: src/views/boards/components/TemplateBoards.tsx:18
|
||||||
msgid "High Priority"
|
msgid "High Priority"
|
||||||
msgstr "High Priority"
|
msgstr "High Priority"
|
||||||
@@ -1366,6 +1399,10 @@ msgstr "Kanban reimagined"
|
|||||||
msgid "Keep in mind that this action is irreversible."
|
msgid "Keep in mind that this action is irreversible."
|
||||||
msgstr "Keep in mind that this action is irreversible."
|
msgstr "Keep in mind that this action is irreversible."
|
||||||
|
|
||||||
|
#: src/providers/keyboard-shortcuts.tsx:320
|
||||||
|
#~ msgid "Keyboard Shortcuts"
|
||||||
|
#~ msgstr "Keyboard Shortcuts"
|
||||||
|
|
||||||
#: src/views/board/components/Filters.tsx:115
|
#: src/views/board/components/Filters.tsx:115
|
||||||
#: src/views/board/components/NewCardForm.tsx:409
|
#: src/views/board/components/NewCardForm.tsx:409
|
||||||
#: src/views/card/index.tsx:119
|
#: src/views/card/index.tsx:119
|
||||||
@@ -1454,7 +1491,7 @@ msgstr "Loved by teams worldwide"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Low Priority"
|
msgstr "Low Priority"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "magic link"
|
msgstr "magic link"
|
||||||
|
|
||||||
@@ -1521,6 +1558,10 @@ msgstr "moved the card to another list"
|
|||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
|
#: src/providers/keyboard-shortcuts.tsx:54
|
||||||
|
#~ msgid "Navigation"
|
||||||
|
#~ msgstr "Navigation"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:95
|
#: src/components/FeedbackModal.tsx:95
|
||||||
msgid "Need help?"
|
msgid "Need help?"
|
||||||
msgstr "Need help?"
|
msgstr "Need help?"
|
||||||
@@ -1601,7 +1642,7 @@ msgstr "Next Steps"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "No {0}"
|
msgstr "No {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "No authentication methods are currently available"
|
msgstr "No authentication methods are currently available"
|
||||||
|
|
||||||
@@ -1621,6 +1662,10 @@ msgstr "No credit card required"
|
|||||||
msgid "No download URL available for this attachment."
|
msgid "No download URL available for this attachment."
|
||||||
msgstr "No download URL available for this attachment."
|
msgstr "No download URL available for this attachment."
|
||||||
|
|
||||||
|
#: src/providers/keyboard-shortcuts.tsx:333
|
||||||
|
#~ msgid "No keyboard shortcuts registered."
|
||||||
|
#~ msgstr "No keyboard shortcuts registered."
|
||||||
|
|
||||||
#: src/views/board/index.tsx:467
|
#: src/views/board/index.tsx:467
|
||||||
msgid "No lists"
|
msgid "No lists"
|
||||||
msgstr "No lists"
|
msgstr "No lists"
|
||||||
@@ -1645,6 +1690,14 @@ msgstr "Once you delete your account, there is no going back. This action cannot
|
|||||||
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
|
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
|
||||||
msgstr "Once you delete your workspace, there is no going back. This action cannot be undone."
|
msgstr "Once you delete your workspace, there is no going back. This action cannot be undone."
|
||||||
|
|
||||||
|
#: src/components/WorkspaceMenu.tsx:31
|
||||||
|
#~ msgid "Open command menu"
|
||||||
|
#~ msgstr "Open command menu"
|
||||||
|
|
||||||
|
#: src/providers/keyboard-shortcuts.tsx:171
|
||||||
|
#~ msgid "Open keyboard shortcuts"
|
||||||
|
#~ msgstr "Open keyboard shortcuts"
|
||||||
|
|
||||||
#: src/views/pricing/components/FeatureComparisonTable.tsx:150
|
#: src/views/pricing/components/FeatureComparisonTable.tsx:150
|
||||||
msgid "Open source"
|
msgid "Open source"
|
||||||
msgstr "Open source"
|
msgstr "Open source"
|
||||||
@@ -1653,7 +1706,7 @@ msgstr "Open source"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Open Source Friends"
|
msgstr "Open Source Friends"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "or"
|
msgstr "or"
|
||||||
|
|
||||||
@@ -1735,15 +1788,15 @@ msgstr "Platform"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Please confirm your new password"
|
msgstr "Please confirm your new password"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Please enter a valid email address"
|
msgstr "Please enter a valid email address"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Please enter a valid name"
|
msgstr "Please enter a valid name"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Please enter a valid password"
|
msgstr "Please enter a valid password"
|
||||||
|
|
||||||
@@ -2038,6 +2091,10 @@ msgstr "Settings | Workspace"
|
|||||||
#~ msgid "Share invite link"
|
#~ msgid "Share invite link"
|
||||||
#~ msgstr "Share invite link"
|
#~ msgstr "Share invite link"
|
||||||
|
|
||||||
|
#: src/components/UserMenu.tsx:184
|
||||||
|
#~ msgid "Shortcuts"
|
||||||
|
#~ msgstr "Shortcuts"
|
||||||
|
|
||||||
#: src/views/home/components/Header.tsx:104
|
#: src/views/home/components/Header.tsx:104
|
||||||
msgid "Sign in"
|
msgid "Sign in"
|
||||||
msgstr "Sign in"
|
msgstr "Sign in"
|
||||||
@@ -2063,7 +2120,7 @@ msgstr "Sign up disabled"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "Sign up is currently disabled. Please try again later."
|
msgstr "Sign up is currently disabled. Please try again later."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "Sign up with "
|
msgstr "Sign up with "
|
||||||
|
|
||||||
@@ -2425,6 +2482,11 @@ msgstr "Unlimited members"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Unlimited workspaces"
|
msgstr "Unlimited workspaces"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Unsubscribe"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2550,6 +2612,10 @@ msgstr "Visibility"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "We are using the <0>AGPL-3.0 license</0>."
|
msgstr "We are using the <0>AGPL-3.0 license</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "We couldn't update your preferences. Please try again."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "We're just getting started. "
|
msgstr "We're just getting started. "
|
||||||
@@ -2700,6 +2766,10 @@ msgstr "You have been logged in successfully."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "You have been signed up successfully."
|
msgstr "You have been signed up successfully."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "You have been unsubscribed!"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgstr "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
@@ -2744,6 +2814,10 @@ msgstr "Your Trello account has been disconnected."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Your Trello account is connected."
|
msgstr "Your Trello account is connected."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "Your workspace description has been updated."
|
msgstr "Your workspace description has been updated."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Cuenta"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Cuenta eliminada"
|
msgstr "Cuenta eliminada"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Actividad"
|
msgstr "Actividad"
|
||||||
@@ -309,7 +309,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Entrada de blog"
|
msgstr "Entrada de blog"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Tablero"
|
msgstr "Tablero"
|
||||||
@@ -391,11 +391,12 @@ msgstr "Informe de error"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Cancelar"
|
msgstr "Cancelar"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Tarjeta"
|
msgstr "Tarjeta"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Tarjeta no encontrada"
|
msgstr "Tarjeta no encontrada"
|
||||||
|
|
||||||
@@ -493,6 +494,10 @@ msgstr "completó un elemento de la lista de verificación"
|
|||||||
msgid "completed checklist item <0>{0}</0>"
|
msgid "completed checklist item <0>{0}</0>"
|
||||||
msgstr "completó el elemento <0>{0}</0> de la lista de verificación"
|
msgstr "completó el elemento <0>{0}</0> de la lista de verificación"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Confirma tus preferencias de correo electrónico:"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Confirma tu nueva contraseña"
|
msgstr "Confirma tu nueva contraseña"
|
||||||
@@ -523,12 +528,12 @@ msgstr "Contáctanos"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Creación de contenido"
|
msgstr "Creación de contenido"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Continuar con "
|
msgstr "Continuar con "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Continuar con {0}"
|
msgstr "Continuar con {0}"
|
||||||
|
|
||||||
@@ -747,6 +752,10 @@ msgstr "Nombre visible actualizado"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "¿Ofrecen un plan gratuito?"
|
msgstr "¿Ofrecen un plan gratuito?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "¿Quieres darte de baja?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "documentos"
|
msgstr "documentos"
|
||||||
@@ -798,7 +807,7 @@ msgstr "Editar URL del espacio de trabajo"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Editando"
|
msgstr "Editando"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "correo electrónico"
|
msgstr "correo electrónico"
|
||||||
|
|
||||||
@@ -818,11 +827,11 @@ msgstr "Introduce tu contraseña actual"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Introduce tu contraseña actual y elige una nueva contraseña segura."
|
msgstr "Introduce tu contraseña actual y elige una nueva contraseña segura."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Introduce tu dirección de correo electrónico"
|
msgstr "Introduce tu dirección de correo electrónico"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Introduce tu nombre"
|
msgstr "Introduce tu nombre"
|
||||||
|
|
||||||
@@ -830,7 +839,7 @@ msgstr "Introduce tu nombre"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Introduce tu nueva contraseña"
|
msgstr "Introduce tu nueva contraseña"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Introduce tu contraseña"
|
msgstr "Introduce tu contraseña"
|
||||||
|
|
||||||
@@ -1000,8 +1009,6 @@ msgid "For long-term sustainability, we recognise all good open source projects
|
|||||||
msgstr "Para la sostenibilidad a largo plazo, reconocemos que todos los buenos proyectos de código abierto necesitan una fuente de ingresos. La nuestra proviene de la oferta de nube de pago para espacios de trabajo con múltiples usuarios y para slugs de espacio de trabajo personalizados. No hay obligación de usarla, y puedes alojar el software en tu propia infraestructura sin costo alguno."
|
msgstr "Para la sostenibilidad a largo plazo, reconocemos que todos los buenos proyectos de código abierto necesitan una fuente de ingresos. La nuestra proviene de la oferta de nube de pago para espacios de trabajo con múltiples usuarios y para slugs de espacio de trabajo personalizados. No hay obligación de usarla, y puedes alojar el software en tu propia infraestructura sin costo alguno."
|
||||||
|
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
#: src/views/pricing/components/Pricing.tsx:32
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
#: src/views/pricing/components/PricingTiers.tsx:30
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Gratis"
|
msgstr "Gratis"
|
||||||
@@ -1345,7 +1352,7 @@ msgstr "Amado por equipos en todo el mundo"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Prioridad baja"
|
msgstr "Prioridad baja"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "enlace mágico"
|
msgstr "enlace mágico"
|
||||||
|
|
||||||
@@ -1476,7 +1483,7 @@ msgstr "Próximos pasos"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "Sin {0}"
|
msgstr "Sin {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "No hay métodos de autenticación disponibles actualmente"
|
msgstr "No hay métodos de autenticación disponibles actualmente"
|
||||||
|
|
||||||
@@ -1524,7 +1531,7 @@ msgstr "Código abierto"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Amigos de código abierto"
|
msgstr "Amigos de código abierto"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "o"
|
msgstr "o"
|
||||||
|
|
||||||
@@ -1606,15 +1613,15 @@ msgstr "Plataforma"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Por favor, confirma tu nueva contraseña"
|
msgstr "Por favor, confirma tu nueva contraseña"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Por favor, introduce una dirección de correo electrónico válida"
|
msgstr "Por favor, introduce una dirección de correo electrónico válida"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Por favor, introduce un nombre válido"
|
msgstr "Por favor, introduce un nombre válido"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Por favor, introduce una contraseña válida"
|
msgstr "Por favor, introduce una contraseña válida"
|
||||||
|
|
||||||
@@ -1914,7 +1921,7 @@ msgstr "Registro deshabilitado"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "El registro está actualmente deshabilitado. Por favor, inténtalo de nuevo más tarde."
|
msgstr "El registro está actualmente deshabilitado. Por favor, inténtalo de nuevo más tarde."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "Registrarse con "
|
msgstr "Registrarse con "
|
||||||
|
|
||||||
@@ -2264,6 +2271,11 @@ msgstr "Miembros ilimitados"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Espacios de trabajo ilimitados"
|
msgstr "Espacios de trabajo ilimitados"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Darse de baja"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2377,6 +2389,10 @@ msgstr "Visibilidad"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "Estamos usando la <0>licencia AGPL-3.0</0>."
|
msgstr "Estamos usando la <0>licencia AGPL-3.0</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "No pudimos actualizar tus preferencias. Por favor, inténtalo de nuevo."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "Apenas estamos comenzando. "
|
msgstr "Apenas estamos comenzando. "
|
||||||
@@ -2515,6 +2531,10 @@ msgstr "Has iniciado sesión correctamente."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "Te has registrado correctamente."
|
msgstr "Te has registrado correctamente."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "¡Te has dado de baja!"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "Tienes plazas ilimitadas con tu Plan Pro. ¡No hay cargos adicionales por nuevos miembros!"
|
msgstr "Tienes plazas ilimitadas con tu Plan Pro. ¡No hay cargos adicionales por nuevos miembros!"
|
||||||
@@ -2559,6 +2579,10 @@ msgstr "Tu cuenta de Trello ha sido desconectada."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Tu cuenta de Trello está conectada."
|
msgstr "Tu cuenta de Trello está conectada."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "A tu enlace de baja le falta un token. Por favor, abre el último correo electrónico e inténtalo de nuevo."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "La descripción de tu espacio de trabajo ha sido actualizada."
|
msgstr "La descripción de tu espacio de trabajo ha sido actualizada."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Compte"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Compte supprimé"
|
msgstr "Compte supprimé"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Activité"
|
msgstr "Activité"
|
||||||
@@ -309,7 +309,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Article de blog"
|
msgstr "Article de blog"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Tableau"
|
msgstr "Tableau"
|
||||||
@@ -391,11 +391,12 @@ msgstr "Rapport de bug"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Annuler"
|
msgstr "Annuler"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Carte"
|
msgstr "Carte"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Carte introuvable"
|
msgstr "Carte introuvable"
|
||||||
|
|
||||||
@@ -493,6 +494,10 @@ msgstr "a terminé un élément de la checklist"
|
|||||||
msgid "completed checklist item <0>{0}</0>"
|
msgid "completed checklist item <0>{0}</0>"
|
||||||
msgstr "a terminé l'élément <0>{0}</0> de la checklist"
|
msgstr "a terminé l'élément <0>{0}</0> de la checklist"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Confirmez vos préférences d'e-mail :"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Confirmez votre nouveau mot de passe"
|
msgstr "Confirmez votre nouveau mot de passe"
|
||||||
@@ -523,12 +528,12 @@ msgstr "Contactez-nous"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Création de contenu"
|
msgstr "Création de contenu"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Continuer avec "
|
msgstr "Continuer avec "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Continuer avec {0}"
|
msgstr "Continuer avec {0}"
|
||||||
|
|
||||||
@@ -747,6 +752,10 @@ msgstr "Nom d'affichage mis à jour"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "Proposez-vous un plan gratuit ?"
|
msgstr "Proposez-vous un plan gratuit ?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "Voulez-vous vous désabonner ?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "documentation"
|
msgstr "documentation"
|
||||||
@@ -798,7 +807,7 @@ msgstr "Modifier l'URL de l'espace de travail"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Édition"
|
msgstr "Édition"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "e-mail"
|
msgstr "e-mail"
|
||||||
|
|
||||||
@@ -818,11 +827,11 @@ msgstr "Saisissez votre mot de passe actuel"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Saisissez votre mot de passe actuel et choisissez un nouveau mot de passe sécurisé."
|
msgstr "Saisissez votre mot de passe actuel et choisissez un nouveau mot de passe sécurisé."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Saisissez votre adresse e-mail"
|
msgstr "Saisissez votre adresse e-mail"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Saisissez votre nom"
|
msgstr "Saisissez votre nom"
|
||||||
|
|
||||||
@@ -830,7 +839,7 @@ msgstr "Saisissez votre nom"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Saisissez votre nouveau mot de passe"
|
msgstr "Saisissez votre nouveau mot de passe"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Saisissez votre mot de passe"
|
msgstr "Saisissez votre mot de passe"
|
||||||
|
|
||||||
@@ -1000,8 +1009,6 @@ msgid "For long-term sustainability, we recognise all good open source projects
|
|||||||
msgstr "Pour une durabilité à long terme, nous reconnaissons que tous les bons projets open source ont besoin d'une source de revenus. La nôtre provient de l'offre cloud payante pour les espaces de travail avec plusieurs utilisateurs et pour les slugs d'espace de travail personnalisés. Il n'y a aucune obligation de l'utiliser, et vous pouvez auto-héberger le logiciel sur votre propre infrastructure gratuitement."
|
msgstr "Pour une durabilité à long terme, nous reconnaissons que tous les bons projets open source ont besoin d'une source de revenus. La nôtre provient de l'offre cloud payante pour les espaces de travail avec plusieurs utilisateurs et pour les slugs d'espace de travail personnalisés. Il n'y a aucune obligation de l'utiliser, et vous pouvez auto-héberger le logiciel sur votre propre infrastructure gratuitement."
|
||||||
|
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
#: src/views/pricing/components/Pricing.tsx:32
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
#: src/views/pricing/components/PricingTiers.tsx:30
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Gratuit"
|
msgstr "Gratuit"
|
||||||
@@ -1345,7 +1352,7 @@ msgstr "Adoré par les équipes du monde entier"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Priorité basse"
|
msgstr "Priorité basse"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "lien magique"
|
msgstr "lien magique"
|
||||||
|
|
||||||
@@ -1476,7 +1483,7 @@ msgstr "Prochaines étapes"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "Aucun {0}"
|
msgstr "Aucun {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "Aucune méthode d'authentification n'est actuellement disponible"
|
msgstr "Aucune méthode d'authentification n'est actuellement disponible"
|
||||||
|
|
||||||
@@ -1524,7 +1531,7 @@ msgstr "Open source"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Amis Open Source"
|
msgstr "Amis Open Source"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "ou"
|
msgstr "ou"
|
||||||
|
|
||||||
@@ -1606,15 +1613,15 @@ msgstr "Plateforme"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Veuillez confirmer votre nouveau mot de passe"
|
msgstr "Veuillez confirmer votre nouveau mot de passe"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Veuillez saisir une adresse e-mail valide"
|
msgstr "Veuillez saisir une adresse e-mail valide"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Veuillez saisir un nom valide"
|
msgstr "Veuillez saisir un nom valide"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Veuillez saisir un mot de passe valide"
|
msgstr "Veuillez saisir un mot de passe valide"
|
||||||
|
|
||||||
@@ -1914,7 +1921,7 @@ msgstr "Inscription désactivée"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "L'inscription est actuellement désactivée. Veuillez réessayer plus tard."
|
msgstr "L'inscription est actuellement désactivée. Veuillez réessayer plus tard."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "S'inscrire avec "
|
msgstr "S'inscrire avec "
|
||||||
|
|
||||||
@@ -2264,6 +2271,11 @@ msgstr "Membres illimités"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Espaces de travail illimités"
|
msgstr "Espaces de travail illimités"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Se désabonner"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2377,6 +2389,10 @@ msgstr "Visibilité"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "Nous utilisons la <0>licence AGPL-3.0</0>."
|
msgstr "Nous utilisons la <0>licence AGPL-3.0</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "Nous n'avons pas pu mettre à jour vos préférences. Veuillez réessayer."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "Nous ne faisons que commencer. "
|
msgstr "Nous ne faisons que commencer. "
|
||||||
@@ -2515,6 +2531,10 @@ msgstr "Vous vous êtes connecté avec succès."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "Vous vous êtes inscrit avec succès."
|
msgstr "Vous vous êtes inscrit avec succès."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "Vous avez été désabonné !"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "Vous disposez de places illimitées avec votre Plan Pro. Il n'y a pas de frais supplémentaires pour les nouveaux membres !"
|
msgstr "Vous disposez de places illimitées avec votre Plan Pro. Il n'y a pas de frais supplémentaires pour les nouveaux membres !"
|
||||||
@@ -2559,6 +2579,10 @@ msgstr "Votre compte Trello a été déconnecté."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Votre compte Trello est connecté."
|
msgstr "Votre compte Trello est connecté."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "Votre lien de désabonnement ne contient pas de jeton. Veuillez ouvrir le dernier e-mail reçu et réessayer."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "La description de votre espace de travail a été mise à jour."
|
msgstr "La description de votre espace de travail a été mise à jour."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Account"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Account eliminato"
|
msgstr "Account eliminato"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Attività"
|
msgstr "Attività"
|
||||||
@@ -309,7 +309,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Post del blog"
|
msgstr "Post del blog"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Bacheca"
|
msgstr "Bacheca"
|
||||||
@@ -391,11 +391,12 @@ msgstr "Segnalazione bug"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Annulla"
|
msgstr "Annulla"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Carta"
|
msgstr "Carta"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Carta non trovata"
|
msgstr "Carta non trovata"
|
||||||
|
|
||||||
@@ -493,6 +494,10 @@ msgstr "ha completato un elemento della checklist"
|
|||||||
msgid "completed checklist item <0>{0}</0>"
|
msgid "completed checklist item <0>{0}</0>"
|
||||||
msgstr "ha completato l'elemento <0>{0}</0> della checklist"
|
msgstr "ha completato l'elemento <0>{0}</0> della checklist"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Conferma le tue preferenze email:"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Conferma la tua nuova password"
|
msgstr "Conferma la tua nuova password"
|
||||||
@@ -523,12 +528,12 @@ msgstr "Contattaci"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Creazione contenuti"
|
msgstr "Creazione contenuti"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Continua con "
|
msgstr "Continua con "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Continua con {0}"
|
msgstr "Continua con {0}"
|
||||||
|
|
||||||
@@ -747,6 +752,10 @@ msgstr "Nome visualizzato aggiornato"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "Offrite un piano gratuito?"
|
msgstr "Offrite un piano gratuito?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "Vuoi annullare l'iscrizione?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "documenti"
|
msgstr "documenti"
|
||||||
@@ -798,7 +807,7 @@ msgstr "Modifica URL dell'area di lavoro"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Modifica"
|
msgstr "Modifica"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "email"
|
msgstr "email"
|
||||||
|
|
||||||
@@ -818,11 +827,11 @@ msgstr "Inserisci la tua password attuale"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Inserisci la tua password attuale e scegli una nuova password sicura."
|
msgstr "Inserisci la tua password attuale e scegli una nuova password sicura."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Inserisci il tuo indirizzo email"
|
msgstr "Inserisci il tuo indirizzo email"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Inserisci il tuo nome"
|
msgstr "Inserisci il tuo nome"
|
||||||
|
|
||||||
@@ -830,7 +839,7 @@ msgstr "Inserisci il tuo nome"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Inserisci la tua nuova password"
|
msgstr "Inserisci la tua nuova password"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Inserisci la tua password"
|
msgstr "Inserisci la tua password"
|
||||||
|
|
||||||
@@ -1000,8 +1009,6 @@ msgid "For long-term sustainability, we recognise all good open source projects
|
|||||||
msgstr "Per la sostenibilità a lungo termine, riconosciamo che tutti i buoni progetti open source necessitano di una fonte di entrate. La nostra proviene dall'offerta cloud a pagamento per gli spazi di lavoro con più utenti e per gli slug personalizzati degli spazi di lavoro. Non c'è alcun obbligo di utilizzarla, e puoi ospitare autonomamente il software sulla tua infrastruttura gratuitamente."
|
msgstr "Per la sostenibilità a lungo termine, riconosciamo che tutti i buoni progetti open source necessitano di una fonte di entrate. La nostra proviene dall'offerta cloud a pagamento per gli spazi di lavoro con più utenti e per gli slug personalizzati degli spazi di lavoro. Non c'è alcun obbligo di utilizzarla, e puoi ospitare autonomamente il software sulla tua infrastruttura gratuitamente."
|
||||||
|
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
#: src/views/pricing/components/Pricing.tsx:32
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
#: src/views/pricing/components/PricingTiers.tsx:30
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Gratuito"
|
msgstr "Gratuito"
|
||||||
@@ -1345,7 +1352,7 @@ msgstr "Amato dai team di tutto il mondo"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Bassa priorità"
|
msgstr "Bassa priorità"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "link magico"
|
msgstr "link magico"
|
||||||
|
|
||||||
@@ -1476,7 +1483,7 @@ msgstr "Prossimi passi"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "Nessun {0}"
|
msgstr "Nessun {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "Nessun metodo di autenticazione è attualmente disponibile"
|
msgstr "Nessun metodo di autenticazione è attualmente disponibile"
|
||||||
|
|
||||||
@@ -1524,7 +1531,7 @@ msgstr "Open source"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Amici Open Source"
|
msgstr "Amici Open Source"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "o"
|
msgstr "o"
|
||||||
|
|
||||||
@@ -1606,15 +1613,15 @@ msgstr "Piattaforma"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Conferma la tua nuova password"
|
msgstr "Conferma la tua nuova password"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Inserisci un indirizzo email valido"
|
msgstr "Inserisci un indirizzo email valido"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Inserisci un nome valido"
|
msgstr "Inserisci un nome valido"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Inserisci una password valida"
|
msgstr "Inserisci una password valida"
|
||||||
|
|
||||||
@@ -1914,7 +1921,7 @@ msgstr "Registrazione disabilitata"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "La registrazione è attualmente disabilitata. Riprova più tardi."
|
msgstr "La registrazione è attualmente disabilitata. Riprova più tardi."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "Registrati con "
|
msgstr "Registrati con "
|
||||||
|
|
||||||
@@ -2264,6 +2271,11 @@ msgstr "Membri illimitati"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Aree di lavoro illimitate"
|
msgstr "Aree di lavoro illimitate"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Annulla iscrizione"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2377,6 +2389,10 @@ msgstr "Visibilità"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "Utilizziamo la <0>licenza AGPL-3.0</0>."
|
msgstr "Utilizziamo la <0>licenza AGPL-3.0</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "Non siamo riusciti ad aggiornare le tue preferenze. Riprova."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "Siamo solo all'inizio. "
|
msgstr "Siamo solo all'inizio. "
|
||||||
@@ -2515,6 +2531,10 @@ msgstr "Hai effettuato l'accesso con successo."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "Ti sei registrato con successo."
|
msgstr "Ti sei registrato con successo."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "La tua iscrizione è stata annullata!"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "Hai posti illimitati con il tuo Piano Pro. Non ci sono costi aggiuntivi per i nuovi membri!"
|
msgstr "Hai posti illimitati con il tuo Piano Pro. Non ci sono costi aggiuntivi per i nuovi membri!"
|
||||||
@@ -2559,6 +2579,10 @@ msgstr "Il tuo account Trello è stato disconnesso."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Il tuo account Trello è connesso."
|
msgstr "Il tuo account Trello è connesso."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "Il tuo link per annullare l'iscrizione non contiene un token. Apri l'email più recente e riprova."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "La descrizione della tua area di lavoro è stata aggiornata."
|
msgstr "La descrizione della tua area di lavoro è stata aggiornata."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Account"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Account verwijderd"
|
msgstr "Account verwijderd"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Activiteit"
|
msgstr "Activiteit"
|
||||||
@@ -309,7 +309,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Blogbericht"
|
msgstr "Blogbericht"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Bord"
|
msgstr "Bord"
|
||||||
@@ -391,11 +391,12 @@ msgstr "Bugrapport"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Annuleren"
|
msgstr "Annuleren"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Kaart"
|
msgstr "Kaart"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Kaart niet gevonden"
|
msgstr "Kaart niet gevonden"
|
||||||
|
|
||||||
@@ -493,6 +494,10 @@ msgstr "heeft een checklistitem voltooid"
|
|||||||
msgid "completed checklist item <0>{0}</0>"
|
msgid "completed checklist item <0>{0}</0>"
|
||||||
msgstr "heeft checklistitem <0>{0}</0> voltooid"
|
msgstr "heeft checklistitem <0>{0}</0> voltooid"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Bevestig je e-mailvoorkeuren:"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Bevestig je nieuwe wachtwoord"
|
msgstr "Bevestig je nieuwe wachtwoord"
|
||||||
@@ -523,12 +528,12 @@ msgstr "Neem contact op"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Content creatie"
|
msgstr "Content creatie"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Doorgaan met "
|
msgstr "Doorgaan met "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Doorgaan met {0}"
|
msgstr "Doorgaan met {0}"
|
||||||
|
|
||||||
@@ -747,6 +752,10 @@ msgstr "Weergavenaam bijgewerkt"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "Bieden jullie een gratis abonnement aan?"
|
msgstr "Bieden jullie een gratis abonnement aan?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "Wil je je uitschrijven?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "docs"
|
msgstr "docs"
|
||||||
@@ -798,7 +807,7 @@ msgstr "Werkruimte-URL bewerken"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Bewerken"
|
msgstr "Bewerken"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "e-mail"
|
msgstr "e-mail"
|
||||||
|
|
||||||
@@ -818,11 +827,11 @@ msgstr "Voer je huidige wachtwoord in"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Voer je huidige wachtwoord in en kies een nieuw veilig wachtwoord."
|
msgstr "Voer je huidige wachtwoord in en kies een nieuw veilig wachtwoord."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Voer je e-mailadres in"
|
msgstr "Voer je e-mailadres in"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Voer je naam in"
|
msgstr "Voer je naam in"
|
||||||
|
|
||||||
@@ -830,7 +839,7 @@ msgstr "Voer je naam in"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Voer je nieuwe wachtwoord in"
|
msgstr "Voer je nieuwe wachtwoord in"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Voer je wachtwoord in"
|
msgstr "Voer je wachtwoord in"
|
||||||
|
|
||||||
@@ -1000,8 +1009,6 @@ msgid "For long-term sustainability, we recognise all good open source projects
|
|||||||
msgstr "Voor duurzaamheid op lange termijn erkennen we dat alle goede open source projecten een inkomstenbron nodig hebben. De onze komt van de betaalde cloudoplossing voor werkruimtes met meerdere gebruikers en voor aangepaste werkruimte-slugs. Er is geen verplichting om het te gebruiken, en je kunt de software gratis op je eigen infrastructuur hosten."
|
msgstr "Voor duurzaamheid op lange termijn erkennen we dat alle goede open source projecten een inkomstenbron nodig hebben. De onze komt van de betaalde cloudoplossing voor werkruimtes met meerdere gebruikers en voor aangepaste werkruimte-slugs. Er is geen verplichting om het te gebruiken, en je kunt de software gratis op je eigen infrastructuur hosten."
|
||||||
|
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
#: src/views/pricing/components/Pricing.tsx:32
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
#: src/views/pricing/components/PricingTiers.tsx:30
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Gratis"
|
msgstr "Gratis"
|
||||||
@@ -1345,7 +1352,7 @@ msgstr "Geliefd door teams wereldwijd"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Lage prioriteit"
|
msgstr "Lage prioriteit"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "magische link"
|
msgstr "magische link"
|
||||||
|
|
||||||
@@ -1476,7 +1483,7 @@ msgstr "Volgende stappen"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "Geen {0}"
|
msgstr "Geen {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "Er zijn momenteel geen authenticatiemethoden beschikbaar"
|
msgstr "Er zijn momenteel geen authenticatiemethoden beschikbaar"
|
||||||
|
|
||||||
@@ -1524,7 +1531,7 @@ msgstr "Open source"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Open Source Vrienden"
|
msgstr "Open Source Vrienden"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "of"
|
msgstr "of"
|
||||||
|
|
||||||
@@ -1606,15 +1613,15 @@ msgstr "Platform"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Bevestig je nieuwe wachtwoord"
|
msgstr "Bevestig je nieuwe wachtwoord"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Voer een geldig e-mailadres in"
|
msgstr "Voer een geldig e-mailadres in"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Voer een geldige naam in"
|
msgstr "Voer een geldige naam in"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Voer een geldig wachtwoord in"
|
msgstr "Voer een geldig wachtwoord in"
|
||||||
|
|
||||||
@@ -1914,7 +1921,7 @@ msgstr "Registreren uitgeschakeld"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "Registreren is momenteel uitgeschakeld. Probeer het later opnieuw."
|
msgstr "Registreren is momenteel uitgeschakeld. Probeer het later opnieuw."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "Registreren met "
|
msgstr "Registreren met "
|
||||||
|
|
||||||
@@ -2264,6 +2271,11 @@ msgstr "Onbeperkt aantal leden"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Onbeperkte werkruimtes"
|
msgstr "Onbeperkte werkruimtes"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Uitschrijven"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2377,6 +2389,10 @@ msgstr "Zichtbaarheid"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "We gebruiken de <0>AGPL-3.0 licentie</0>."
|
msgstr "We gebruiken de <0>AGPL-3.0 licentie</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "We konden je voorkeuren niet bijwerken. Probeer het opnieuw."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "We zijn nog maar net begonnen. "
|
msgstr "We zijn nog maar net begonnen. "
|
||||||
@@ -2515,6 +2531,10 @@ msgstr "Je bent succesvol ingelogd."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "Je bent succesvol geregistreerd."
|
msgstr "Je bent succesvol geregistreerd."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "Je bent uitgeschreven!"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "Je hebt onbeperkte plaatsen met je Pro Plan. Er zijn geen extra kosten voor nieuwe leden!"
|
msgstr "Je hebt onbeperkte plaatsen met je Pro Plan. Er zijn geen extra kosten voor nieuwe leden!"
|
||||||
@@ -2559,6 +2579,10 @@ msgstr "Je Trello-account is ontkoppeld."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Je Trello-account is verbonden."
|
msgstr "Je Trello-account is verbonden."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "Je uitschrijflink mist een token. Open de meest recente e-mail en probeer het opnieuw."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "Je werkruimtebeschrijving is bijgewerkt."
|
msgstr "Je werkruimtebeschrijving is bijgewerkt."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -36,7 +36,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ msgstr "Konto"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Konto zostało usunięte"
|
msgstr "Konto zostało usunięte"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Aktywność"
|
msgstr "Aktywność"
|
||||||
@@ -314,7 +314,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Post na blogu"
|
msgstr "Post na blogu"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Tablica"
|
msgstr "Tablica"
|
||||||
@@ -396,11 +396,12 @@ msgstr "Zgłoszenie błędu"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Anuluj"
|
msgstr "Anuluj"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Karta"
|
msgstr "Karta"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Nie można było znaleźć karty"
|
msgstr "Nie można było znaleźć karty"
|
||||||
|
|
||||||
@@ -498,6 +499,10 @@ msgstr "ukończono element listy kontrolnej"
|
|||||||
msgid "completed checklist item <0>{0}</0>"
|
msgid "completed checklist item <0>{0}</0>"
|
||||||
msgstr "ukończono element listy kontrolnej <0>{0}</0>"
|
msgstr "ukończono element listy kontrolnej <0>{0}</0>"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Potwierdź swoje preferencje dotyczące e-maili:"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Potwierdź swoje nowe hasło"
|
msgstr "Potwierdź swoje nowe hasło"
|
||||||
@@ -528,12 +533,12 @@ msgstr "Skontaktuj się z nami"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Tworzenie treści"
|
msgstr "Tworzenie treści"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Kontynuuj z "
|
msgstr "Kontynuuj z "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Kontynuuj z {0}"
|
msgstr "Kontynuuj z {0}"
|
||||||
|
|
||||||
@@ -752,6 +757,10 @@ msgstr "Zaktualizowano wyświetlaną nazwę"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "Czy oferujecie darmowy plan?"
|
msgstr "Czy oferujecie darmowy plan?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "Czy chcesz się wypisać?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "dokumentacja"
|
msgstr "dokumentacja"
|
||||||
@@ -803,7 +812,7 @@ msgstr "Edytuj URL przestrzeni roboczej"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Edycja"
|
msgstr "Edycja"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "email"
|
msgstr "email"
|
||||||
|
|
||||||
@@ -823,11 +832,11 @@ msgstr "Wprowadź swoje obecne hasło"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Wprowadź swoje obecne hasło i wybierz nowe, bezpieczne hasło."
|
msgstr "Wprowadź swoje obecne hasło i wybierz nowe, bezpieczne hasło."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Wprowadź swój adres e-mail"
|
msgstr "Wprowadź swój adres e-mail"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Wprowadź swoje imię"
|
msgstr "Wprowadź swoje imię"
|
||||||
|
|
||||||
@@ -835,7 +844,7 @@ msgstr "Wprowadź swoje imię"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Wprowadź swoje nowe hasło"
|
msgstr "Wprowadź swoje nowe hasło"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Wprowadź swoje hasło"
|
msgstr "Wprowadź swoje hasło"
|
||||||
|
|
||||||
@@ -1005,8 +1014,6 @@ msgid "For long-term sustainability, we recognise all good open source projects
|
|||||||
msgstr "Dla długoterminowej trwałości uznajemy, że wszystkie dobre projekty open source potrzebują źródła dochodu. Nasze pochodzi z płatnej oferty w chmurze dla przestrzeni roboczych z wieloma użytkownikami oraz dla niestandardowych nazw przestrzeni roboczych. Nie ma obowiązku korzystania z niej, a oprogramowanie można hostować samodzielnie na własnej infrastrukturze bezpłatnie."
|
msgstr "Dla długoterminowej trwałości uznajemy, że wszystkie dobre projekty open source potrzebują źródła dochodu. Nasze pochodzi z płatnej oferty w chmurze dla przestrzeni roboczych z wieloma użytkownikami oraz dla niestandardowych nazw przestrzeni roboczych. Nie ma obowiązku korzystania z niej, a oprogramowanie można hostować samodzielnie na własnej infrastrukturze bezpłatnie."
|
||||||
|
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
#: src/views/pricing/components/Pricing.tsx:32
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
#: src/views/pricing/components/PricingTiers.tsx:30
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Darmowy"
|
msgstr "Darmowy"
|
||||||
@@ -1350,7 +1357,7 @@ msgstr "Uwielbiane przez zespoły na całym świecie"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Niski priorytet"
|
msgstr "Niski priorytet"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "magiczny link"
|
msgstr "magiczny link"
|
||||||
|
|
||||||
@@ -1481,7 +1488,7 @@ msgstr "Następne kroki"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "Brak {0}"
|
msgstr "Brak {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "Obecnie brak dostępnych metod uwierzytelniania"
|
msgstr "Obecnie brak dostępnych metod uwierzytelniania"
|
||||||
|
|
||||||
@@ -1529,7 +1536,7 @@ msgstr "Otwartoźródłowe"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Przyjaciele Open Source"
|
msgstr "Przyjaciele Open Source"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "lub"
|
msgstr "lub"
|
||||||
|
|
||||||
@@ -1611,15 +1618,15 @@ msgstr "Platforma"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Proszę potwierdzić nowe hasło"
|
msgstr "Proszę potwierdzić nowe hasło"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Proszę wprowadzić prawidłowy adres e-mail"
|
msgstr "Proszę wprowadzić prawidłowy adres e-mail"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Proszę wprowadzić poprawne imię"
|
msgstr "Proszę wprowadzić poprawne imię"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Proszę wprowadzić poprawne hasło"
|
msgstr "Proszę wprowadzić poprawne hasło"
|
||||||
|
|
||||||
@@ -1919,7 +1926,7 @@ msgstr "Rejestracja wyłączona"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "Rejestracja jest obecnie wyłączona. Spróbuj ponownie później."
|
msgstr "Rejestracja jest obecnie wyłączona. Spróbuj ponownie później."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "Zarejestruj się za pomocą "
|
msgstr "Zarejestruj się za pomocą "
|
||||||
|
|
||||||
@@ -2269,6 +2276,11 @@ msgstr "Nieograniczona liczba członków"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Nieograniczona liczba przestrzeni roboczych"
|
msgstr "Nieograniczona liczba przestrzeni roboczych"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Wypisz się"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2382,6 +2394,10 @@ msgstr "Widoczność"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "Używamy licencji <0>AGPL-3.0</0>."
|
msgstr "Używamy licencji <0>AGPL-3.0</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "Nie udało się zaktualizować twoich preferencji. Spróbuj ponownie."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "Dopiero zaczynamy. "
|
msgstr "Dopiero zaczynamy. "
|
||||||
@@ -2520,6 +2536,10 @@ msgstr "Zostałeś pomyślnie zalogowany."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "Zostałeś pomyślnie zarejestrowany."
|
msgstr "Zostałeś pomyślnie zarejestrowany."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "Zostałeś wypisany!"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "Masz nielimitowaną liczbę miejsc w swoim planie Pro. Nie ma dodatkowych opłat za nowych członków!"
|
msgstr "Masz nielimitowaną liczbę miejsc w swoim planie Pro. Nie ma dodatkowych opłat za nowych członków!"
|
||||||
@@ -2564,6 +2584,10 @@ msgstr "Twoje konto Trello zostało odłączone."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Twoje konto Trello jest połączone."
|
msgstr "Twoje konto Trello jest połączone."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "Twój link do wypisania się nie zawiera tokena. Otwórz najnowszy e-mail i spróbuj ponownie."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "Opis twojej przestrzeni roboczej został zaktualizowany."
|
msgstr "Opis twojej przestrzeni roboczej został zaktualizowany."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@ msgstr "{0}"
|
|||||||
#. placeholder {1}: board?.name ?? t`Board`
|
#. placeholder {1}: board?.name ?? t`Board`
|
||||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||||
#: src/views/boards/index.tsx:22
|
#: src/views/boards/index.tsx:22
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "{0} | {1}"
|
msgid "{0} | {1}"
|
||||||
msgstr "{0} | {1}"
|
msgstr "{0} | {1}"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Аккаунт"
|
|||||||
msgid "Account deleted"
|
msgid "Account deleted"
|
||||||
msgstr "Аккаунт удалён"
|
msgstr "Аккаунт удалён"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:308
|
#: src/views/card/index.tsx:343
|
||||||
#: src/views/public/board/CardModal.tsx:157
|
#: src/views/public/board/CardModal.tsx:157
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "Активность"
|
msgstr "Активность"
|
||||||
@@ -309,7 +309,7 @@ msgid "Blog Post"
|
|||||||
msgstr "Пост в блоге"
|
msgstr "Пост в блоге"
|
||||||
|
|
||||||
#: src/views/board/index.tsx:363
|
#: src/views/board/index.tsx:363
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
#: src/views/public/board/index.tsx:101
|
#: src/views/public/board/index.tsx:101
|
||||||
msgid "Board"
|
msgid "Board"
|
||||||
msgstr "Доска"
|
msgstr "Доска"
|
||||||
@@ -391,11 +391,12 @@ msgstr "Отчет об ошибке"
|
|||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Отмена"
|
msgstr "Отмена"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:218
|
#: src/views/card/index.tsx:229
|
||||||
msgid "Card"
|
msgid "Card"
|
||||||
msgstr "Карточка"
|
msgstr "Карточка"
|
||||||
|
|
||||||
#: src/views/card/index.tsx:264
|
#: src/views/card/index.tsx:263
|
||||||
|
#: src/views/card/index.tsx:299
|
||||||
msgid "Card not found"
|
msgid "Card not found"
|
||||||
msgstr "Карточка не найдена"
|
msgstr "Карточка не найдена"
|
||||||
|
|
||||||
@@ -493,6 +494,10 @@ msgstr "завершил элемент контрольного списка"
|
|||||||
msgid "completed checklist item <0>{0}</0>"
|
msgid "completed checklist item <0>{0}</0>"
|
||||||
msgstr "завершил элемент контрольного списка <0>{0}</0>"
|
msgstr "завершил элемент контрольного списка <0>{0}</0>"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:81
|
||||||
|
msgid "Confirm your email preferences:"
|
||||||
|
msgstr "Подтвердите ваши настройки электронной почты:"
|
||||||
|
|
||||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
#: src/views/settings/components/ChangePasswordConfirmation.tsx:150
|
||||||
msgid "Confirm your new password"
|
msgid "Confirm your new password"
|
||||||
msgstr "Подтвердите ваш новый пароль"
|
msgstr "Подтвердите ваш новый пароль"
|
||||||
@@ -523,12 +528,12 @@ msgstr "Свяжитесь с нами"
|
|||||||
msgid "Content Creation"
|
msgid "Content Creation"
|
||||||
msgstr "Создание контента"
|
msgstr "Создание контента"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Continue with "
|
msgid "Continue with "
|
||||||
msgstr "Продолжить с "
|
msgstr "Продолжить с "
|
||||||
|
|
||||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||||
#: src/components/AuthForm.tsx:351
|
#: src/components/AuthForm.tsx:355
|
||||||
msgid "Continue with {0}"
|
msgid "Continue with {0}"
|
||||||
msgstr "Продолжить с {0}"
|
msgstr "Продолжить с {0}"
|
||||||
|
|
||||||
@@ -747,6 +752,10 @@ msgstr "Отображаемое имя обновлено"
|
|||||||
msgid "Do you offer a free plan?"
|
msgid "Do you offer a free plan?"
|
||||||
msgstr "Вы предлагаете бесплатный план?"
|
msgstr "Вы предлагаете бесплатный план?"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:78
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr "Вы хотите отписаться?"
|
||||||
|
|
||||||
#: src/components/FeedbackModal.tsx:109
|
#: src/components/FeedbackModal.tsx:109
|
||||||
msgid "docs"
|
msgid "docs"
|
||||||
msgstr "документы"
|
msgstr "документы"
|
||||||
@@ -798,7 +807,7 @@ msgstr "Редактировать URL рабочей области"
|
|||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Редактирование"
|
msgstr "Редактирование"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr "email"
|
msgstr "email"
|
||||||
|
|
||||||
@@ -818,11 +827,11 @@ msgstr "Введите ваш текущий пароль"
|
|||||||
msgid "Enter your current password and choose a new secure password."
|
msgid "Enter your current password and choose a new secure password."
|
||||||
msgstr "Введите ваш текущий пароль и выберите новый безопасный пароль."
|
msgstr "Введите ваш текущий пароль и выберите новый безопасный пароль."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:398
|
#: src/components/AuthForm.tsx:402
|
||||||
msgid "Enter your email address"
|
msgid "Enter your email address"
|
||||||
msgstr "Введите ваш адрес электронной почты"
|
msgstr "Введите ваш адрес электронной почты"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:384
|
#: src/components/AuthForm.tsx:388
|
||||||
msgid "Enter your name"
|
msgid "Enter your name"
|
||||||
msgstr "Введите ваше имя"
|
msgstr "Введите ваше имя"
|
||||||
|
|
||||||
@@ -830,7 +839,7 @@ msgstr "Введите ваше имя"
|
|||||||
msgid "Enter your new password"
|
msgid "Enter your new password"
|
||||||
msgstr "Введите ваш новый пароль"
|
msgstr "Введите ваш новый пароль"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:411
|
#: src/components/AuthForm.tsx:416
|
||||||
msgid "Enter your password"
|
msgid "Enter your password"
|
||||||
msgstr "Введите ваш пароль"
|
msgstr "Введите ваш пароль"
|
||||||
|
|
||||||
@@ -1000,8 +1009,6 @@ msgid "For long-term sustainability, we recognise all good open source projects
|
|||||||
msgstr "Для долгосрочной устойчивости мы признаем, что всем хорошим проектам с открытым исходным кодом необходим источник дохода. Наш доход поступает от платного облачного предложения для рабочих пространств с несколькими пользователями и для пользовательских идентификаторов рабочих пространств. Нет обязательства использовать его, и вы можете самостоятельно разместить программное обеспечение на своей инфраструктуре бесплатно."
|
msgstr "Для долгосрочной устойчивости мы признаем, что всем хорошим проектам с открытым исходным кодом необходим источник дохода. Наш доход поступает от платного облачного предложения для рабочих пространств с несколькими пользователями и для пользовательских идентификаторов рабочих пространств. Нет обязательства использовать его, и вы можете самостоятельно разместить программное обеспечение на своей инфраструктуре бесплатно."
|
||||||
|
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
#: src/views/pricing/components/Pricing.tsx:32
|
||||||
#: src/views/pricing/components/Pricing.tsx:32
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
|
||||||
#: src/views/pricing/components/PricingTiers.tsx:30
|
#: src/views/pricing/components/PricingTiers.tsx:30
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Бесплатно"
|
msgstr "Бесплатно"
|
||||||
@@ -1345,7 +1352,7 @@ msgstr "Любим командами по всему миру"
|
|||||||
msgid "Low Priority"
|
msgid "Low Priority"
|
||||||
msgstr "Низкий приоритет"
|
msgstr "Низкий приоритет"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:435
|
#: src/components/AuthForm.tsx:441
|
||||||
msgid "magic link"
|
msgid "magic link"
|
||||||
msgstr "магическая ссылка"
|
msgstr "магическая ссылка"
|
||||||
|
|
||||||
@@ -1476,7 +1483,7 @@ msgstr "Следующие шаги"
|
|||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr "Нет {0}"
|
msgstr "Нет {0}"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:365
|
#: src/components/AuthForm.tsx:369
|
||||||
msgid "No authentication methods are currently available"
|
msgid "No authentication methods are currently available"
|
||||||
msgstr "В настоящее время нет доступных методов аутентификации"
|
msgstr "В настоящее время нет доступных методов аутентификации"
|
||||||
|
|
||||||
@@ -1524,7 +1531,7 @@ msgstr "С открытым исходным кодом"
|
|||||||
msgid "Open Source Friends"
|
msgid "Open Source Friends"
|
||||||
msgstr "Друзья с открытым исходным кодом"
|
msgstr "Друзья с открытым исходным кодом"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:374
|
#: src/components/AuthForm.tsx:378
|
||||||
msgid "or"
|
msgid "or"
|
||||||
msgstr "или"
|
msgstr "или"
|
||||||
|
|
||||||
@@ -1606,15 +1613,15 @@ msgstr "Платформа"
|
|||||||
msgid "Please confirm your new password"
|
msgid "Please confirm your new password"
|
||||||
msgstr "Пожалуйста, подтвердите новый пароль"
|
msgstr "Пожалуйста, подтвердите новый пароль"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:402
|
#: src/components/AuthForm.tsx:406
|
||||||
msgid "Please enter a valid email address"
|
msgid "Please enter a valid email address"
|
||||||
msgstr "Пожалуйста, введите действительный адрес электронной почты"
|
msgstr "Пожалуйста, введите действительный адрес электронной почты"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:388
|
#: src/components/AuthForm.tsx:392
|
||||||
msgid "Please enter a valid name"
|
msgid "Please enter a valid name"
|
||||||
msgstr "Пожалуйста, введите действительное имя"
|
msgstr "Пожалуйста, введите действительное имя"
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:416
|
#: src/components/AuthForm.tsx:421
|
||||||
msgid "Please enter a valid password"
|
msgid "Please enter a valid password"
|
||||||
msgstr "Пожалуйста, введите действительный пароль"
|
msgstr "Пожалуйста, введите действительный пароль"
|
||||||
|
|
||||||
@@ -1914,7 +1921,7 @@ msgstr "Регистрация отключена"
|
|||||||
msgid "Sign up is currently disabled. Please try again later."
|
msgid "Sign up is currently disabled. Please try again later."
|
||||||
msgstr "Регистрация в настоящее время отключена. Пожалуйста, попробуйте позже."
|
msgstr "Регистрация в настоящее время отключена. Пожалуйста, попробуйте позже."
|
||||||
|
|
||||||
#: src/components/AuthForm.tsx:434
|
#: src/components/AuthForm.tsx:440
|
||||||
msgid "Sign up with "
|
msgid "Sign up with "
|
||||||
msgstr "Зарегистрироваться с "
|
msgstr "Зарегистрироваться с "
|
||||||
|
|
||||||
@@ -2264,6 +2271,11 @@ msgstr "Неограниченное количество участников"
|
|||||||
msgid "Unlimited workspaces"
|
msgid "Unlimited workspaces"
|
||||||
msgstr "Неограниченное количество рабочих пространств"
|
msgstr "Неограниченное количество рабочих пространств"
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:68
|
||||||
|
#: src/pages/unsubscribe/index.tsx:93
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr "Отписаться"
|
||||||
|
|
||||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||||
@@ -2377,6 +2389,10 @@ msgstr "Видимость"
|
|||||||
msgid "We are using the <0>AGPL-3.0 license</0>."
|
msgid "We are using the <0>AGPL-3.0 license</0>."
|
||||||
msgstr "Мы используем лицензию <0>AGPL-3.0</0>."
|
msgstr "Мы используем лицензию <0>AGPL-3.0</0>."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:63
|
||||||
|
msgid "We couldn't update your preferences. Please try again."
|
||||||
|
msgstr "Мы не смогли обновить ваши настройки. Пожалуйста, попробуйте снова."
|
||||||
|
|
||||||
#: src/views/home/components/Features.tsx:152
|
#: src/views/home/components/Features.tsx:152
|
||||||
msgid "We're just getting started. "
|
msgid "We're just getting started. "
|
||||||
msgstr "Мы только начинаем. "
|
msgstr "Мы только начинаем. "
|
||||||
@@ -2515,6 +2531,10 @@ msgstr "Вы успешно вошли в систему."
|
|||||||
msgid "You have been signed up successfully."
|
msgid "You have been signed up successfully."
|
||||||
msgstr "Вы успешно зарегистрировались."
|
msgstr "Вы успешно зарегистрировались."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:98
|
||||||
|
msgid "You have been unsubscribed!"
|
||||||
|
msgstr "Вы успешно отписались!"
|
||||||
|
|
||||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||||
msgstr "В вашем профессиональном плане неограниченное количество мест. За новых участников дополнительная плата не взимается!"
|
msgstr "В вашем профессиональном плане неограниченное количество мест. За новых участников дополнительная плата не взимается!"
|
||||||
@@ -2559,6 +2579,10 @@ msgstr "Ваш аккаунт Trello был отключён."
|
|||||||
msgid "Your Trello account is connected."
|
msgid "Your Trello account is connected."
|
||||||
msgstr "Ваш аккаунт Trello подключён."
|
msgstr "Ваш аккаунт Trello подключён."
|
||||||
|
|
||||||
|
#: src/pages/unsubscribe/index.tsx:33
|
||||||
|
msgid "Your unsubscribe link is missing a token. Please open the latest email and try again."
|
||||||
|
msgstr "В вашей ссылке для отписки отсутствует токен. Пожалуйста, откройте последнее письмо и попробуйте снова."
|
||||||
|
|
||||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:49
|
||||||
msgid "Your workspace description has been updated."
|
msgid "Your workspace description has been updated."
|
||||||
msgstr "Описание вашего рабочего пространства было обновлено."
|
msgstr "Описание вашего рабочего пространства было обновлено."
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ import posthog from "posthog-js";
|
|||||||
import { PostHogProvider } from "posthog-js/react";
|
import { PostHogProvider } from "posthog-js/react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { KeyboardShortcutProvider } from "~/providers/keyboard-shortcuts";
|
||||||
import { LinguiProviderWrapper } from "~/providers/lingui";
|
import { LinguiProviderWrapper } from "~/providers/lingui";
|
||||||
import { ModalProvider } from "~/providers/modal";
|
import { ModalProvider } from "~/providers/modal";
|
||||||
import { PopupProvider } from "~/providers/popup";
|
import { PopupProvider } from "~/providers/popup";
|
||||||
@@ -80,21 +81,23 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
|
|||||||
)}
|
)}
|
||||||
<script src="/__ENV.js" />
|
<script src="/__ENV.js" />
|
||||||
<main className="font-sans">
|
<main className="font-sans">
|
||||||
<LinguiProviderWrapper>
|
<KeyboardShortcutProvider>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<LinguiProviderWrapper>
|
||||||
<ModalProvider>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<PopupProvider>
|
<ModalProvider>
|
||||||
{posthogKey ? (
|
<PopupProvider>
|
||||||
<PostHogProvider client={posthog}>
|
{posthogKey ? (
|
||||||
{getLayout(<Component {...pageProps} />)}
|
<PostHogProvider client={posthog}>
|
||||||
</PostHogProvider>
|
{getLayout(<Component {...pageProps} />)}
|
||||||
) : (
|
</PostHogProvider>
|
||||||
getLayout(<Component {...pageProps} />)
|
) : (
|
||||||
)}
|
getLayout(<Component {...pageProps} />)
|
||||||
</PopupProvider>
|
)}
|
||||||
</ModalProvider>
|
</PopupProvider>
|
||||||
</ThemeProvider>
|
</ModalProvider>
|
||||||
</LinguiProviderWrapper>
|
</ThemeProvider>
|
||||||
|
</LinguiProviderWrapper>
|
||||||
|
</KeyboardShortcutProvider>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const downloadUrl = decodeURIComponent(url);
|
const downloadFilename = typeof filename === "string"
|
||||||
const downloadFilename =
|
? encodeURIComponent(filename)
|
||||||
(typeof filename === "string" ? decodeURIComponent(filename) : null) ??
|
: "attachment";
|
||||||
"attachment";
|
|
||||||
|
|
||||||
const upstream = await fetch(downloadUrl);
|
const upstream = await fetch(url);
|
||||||
|
|
||||||
if (!upstream.ok) {
|
if (!upstream.ok) {
|
||||||
return res.status(upstream.status).json({
|
return res.status(upstream.status).json({
|
||||||
@@ -36,7 +35,7 @@ export default async function handler(
|
|||||||
res.setHeader("Content-Type", contentType);
|
res.setHeader("Content-Type", contentType);
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
`attachment; filename="${downloadFilename}"`,
|
`attachment; filename="${downloadFilename}"; filename*=UTF-8''${downloadFilename}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const buffer = await upstream.arrayBuffer();
|
const buffer = await upstream.arrayBuffer();
|
||||||
|
|||||||
104
apps/web/src/pages/api/unsubscribe.ts
Normal file
104
apps/web/src/pages/api/unsubscribe.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { Novu } from "@novu/api";
|
||||||
|
import { jwtVerify } from "jose";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { env } from "~/env";
|
||||||
|
|
||||||
|
const requestSchema = z.object({
|
||||||
|
token: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenPayloadSchema = z.object({
|
||||||
|
subscriberId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ResponseData =
|
||||||
|
| { success: true }
|
||||||
|
| { success: false; error: string; code?: string };
|
||||||
|
|
||||||
|
const textEncoder = new TextEncoder();
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<ResponseData>,
|
||||||
|
) {
|
||||||
|
if (process.env.NEXT_PUBLIC_KAN_ENV !== "cloud") {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: "Unsubscribe endpoint is not available.",
|
||||||
|
code: "UNAVAILABLE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res.setHeader("Allow", "POST");
|
||||||
|
return res.status(405).json({
|
||||||
|
success: false,
|
||||||
|
error: "Method not allowed.",
|
||||||
|
code: "METHOD_NOT_ALLOWED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = requestSchema.safeParse(req.body);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: "Invalid request payload.",
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!env.EMAIL_UNSUBSCRIBE_SECRET || !env.NOVU_API_KEY) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: "Unsubscribe service is not configured.",
|
||||||
|
code: "NOT_CONFIGURED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: z.infer<typeof tokenPayloadSchema>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verified = await jwtVerify(
|
||||||
|
parsedBody.data.token,
|
||||||
|
textEncoder.encode(env.EMAIL_UNSUBSCRIBE_SECRET),
|
||||||
|
{
|
||||||
|
// We intentionally do not use exp/iat claims –
|
||||||
|
// tokens are long-lived and validated only by signature + payload.
|
||||||
|
clockTolerance: "0s",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
payload = tokenPayloadSchema.parse(verified.payload);
|
||||||
|
} catch {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
error: "Your unsubscribe link is invalid or has expired.",
|
||||||
|
code: "INVALID_TOKEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const novu = new Novu({ secretKey: env.NOVU_API_KEY });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await novu.subscribers.preferences.update(
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
email: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
payload.subscriberId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update Novu preferences", error);
|
||||||
|
return res.status(502).json({
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
"We could not update your email preferences right now. Please try again later.",
|
||||||
|
code: "NOVU_ERROR",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ success: true });
|
||||||
|
}
|
||||||
110
apps/web/src/pages/unsubscribe/index.tsx
Normal file
110
apps/web/src/pages/unsubscribe/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import Button from "~/components/Button";
|
||||||
|
import { PageHead } from "~/components/PageHead";
|
||||||
|
import PatternedBackground from "~/components/PatternedBackground";
|
||||||
|
|
||||||
|
type UnsubscribeStatus = "idle" | "processing" | "success" | "error";
|
||||||
|
|
||||||
|
export default function UnsubscribePage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
const [status, setStatus] = useState<UnsubscribeStatus>("idle");
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) return;
|
||||||
|
const value = router.query.token;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
setToken(value);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
setToken(value[0] ?? "");
|
||||||
|
} else {
|
||||||
|
setToken("");
|
||||||
|
}
|
||||||
|
}, [router.isReady, router.query.token]);
|
||||||
|
|
||||||
|
const handleUnsubscribe = async () => {
|
||||||
|
if (!token) {
|
||||||
|
setStatus("error");
|
||||||
|
setErrorMessage(
|
||||||
|
t`Your unsubscribe link is missing a token. Please open the latest email and try again.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("processing");
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/unsubscribe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const payload = (await response.json().catch(() => null)) as {
|
||||||
|
error?: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
payload?.error ??
|
||||||
|
"We couldn't update your preferences. Please try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("success");
|
||||||
|
} catch (error) {
|
||||||
|
setStatus("error");
|
||||||
|
setErrorMessage(
|
||||||
|
t`We couldn't update your preferences. Please try again.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = t`Unsubscribe`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead title={`${title} | kan.bn`} />
|
||||||
|
<div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<PatternedBackground />
|
||||||
|
<div className="z-10 w-full max-w-md space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||||
|
{t`Do you want to unsubscribe?`}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-center text-sm text-light-900 dark:text-dark-800">
|
||||||
|
{t`Confirm your email preferences:`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleUnsubscribe}
|
||||||
|
disabled={status === "success"}
|
||||||
|
isLoading={status === "processing"}
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
{t`Unsubscribe`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{status === "success" && (
|
||||||
|
<p className="text-center text-sm text-light-900 dark:text-dark-800">
|
||||||
|
{t`You have been unsubscribed!`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{status === "error" && (
|
||||||
|
<p className="mx-auto max-w-[300px] text-center text-sm font-medium text-red-600 dark:text-red-400">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
592
apps/web/src/providers/keyboard-shortcuts.tsx
Normal file
592
apps/web/src/providers/keyboard-shortcuts.tsx
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBackdrop,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@headlessui/react";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { HiXMark } from "react-icons/hi2";
|
||||||
|
|
||||||
|
import { env } from "~/env";
|
||||||
|
import { useEventListener } from "~/hooks/useEventListener";
|
||||||
|
|
||||||
|
const ModifierKey = {
|
||||||
|
CONTROL: "CONTROL",
|
||||||
|
META: "META",
|
||||||
|
ALT: "ALT",
|
||||||
|
SHIFT: "SHIFT",
|
||||||
|
} as const;
|
||||||
|
type ModifierKey = (typeof ModifierKey)[keyof typeof ModifierKey];
|
||||||
|
|
||||||
|
const ModifierKeyInfo: Record<
|
||||||
|
ModifierKey,
|
||||||
|
{
|
||||||
|
macSymbol: string;
|
||||||
|
winName: string;
|
||||||
|
linuxName: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
CONTROL: { macSymbol: "⌃", winName: "Ctrl", linuxName: "Ctrl" },
|
||||||
|
META: { macSymbol: "⌘", winName: "Win", linuxName: "Super" },
|
||||||
|
ALT: { macSymbol: "⌥", winName: "Alt", linuxName: "Alt" },
|
||||||
|
SHIFT: { macSymbol: "⇧", winName: "Shift", linuxName: "Shift" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShortcutGroup = {
|
||||||
|
GENERAL: "GENERAL",
|
||||||
|
NAVIGATION: "NAVIGATION",
|
||||||
|
ACTIONS: "ACTIONS",
|
||||||
|
} as const;
|
||||||
|
type ShortcutGroup = (typeof ShortcutGroup)[keyof typeof ShortcutGroup];
|
||||||
|
|
||||||
|
const getShortcutGroupInfo = (): Record<ShortcutGroup, { label: string }> => ({
|
||||||
|
GENERAL: { label: t`General` },
|
||||||
|
NAVIGATION: { label: t`Navigation` },
|
||||||
|
ACTIONS: { label: t`Actions` },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface KeyStroke {
|
||||||
|
key: string;
|
||||||
|
modifiers?: ModifierKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Press {
|
||||||
|
type: "PRESS";
|
||||||
|
stroke: KeyStroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Sequence {
|
||||||
|
type: "SEQUENCE";
|
||||||
|
strokes: KeyStroke[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeyboardShortcut = {
|
||||||
|
action: () => void;
|
||||||
|
description: string;
|
||||||
|
group: ShortcutGroup;
|
||||||
|
} & (Press | Sequence);
|
||||||
|
|
||||||
|
interface ShortcutTreeStepNode {
|
||||||
|
type: "STEP";
|
||||||
|
children: ShortcutTreeLevel;
|
||||||
|
}
|
||||||
|
interface ShortcutTreeActionNode {
|
||||||
|
type: "ACTION";
|
||||||
|
shortcut: KeyboardShortcut;
|
||||||
|
}
|
||||||
|
type ShortcutTreeNode = ShortcutTreeStepNode | ShortcutTreeActionNode;
|
||||||
|
type ShortcutTreeLevel = Record<string, ShortcutTreeNode>;
|
||||||
|
|
||||||
|
const SEQUENCE_TIMEOUT_MS = 1000;
|
||||||
|
|
||||||
|
const ShortcutConflictCode = {
|
||||||
|
ACTION_ON_SEQUENCE: "ACTION_ON_SEQUENCE",
|
||||||
|
DUPLICATE_ACTION: "DUPLICATE_ACTION",
|
||||||
|
SEQUENCE_ON_ACTION: "SEQUENCE_ON_ACTION",
|
||||||
|
} as const;
|
||||||
|
type ShortcutConflictCode = keyof typeof ShortcutConflictCode;
|
||||||
|
|
||||||
|
interface ShortcutConflictErrorOptions {
|
||||||
|
code: ShortcutConflictCode;
|
||||||
|
shortcut: KeyboardShortcut;
|
||||||
|
conflictPath: string;
|
||||||
|
existingShortcut?: KeyboardShortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShortcutConflictError extends Error {
|
||||||
|
public readonly code: ShortcutConflictCode;
|
||||||
|
public readonly shortcut: KeyboardShortcut;
|
||||||
|
public readonly conflictPath: string;
|
||||||
|
public readonly existingShortcut?: KeyboardShortcut;
|
||||||
|
|
||||||
|
constructor(options: ShortcutConflictErrorOptions) {
|
||||||
|
super(ShortcutConflictError.formatMessage(options));
|
||||||
|
this.name = "ShortcutConflictError";
|
||||||
|
this.code = options.code;
|
||||||
|
this.shortcut = options.shortcut;
|
||||||
|
this.conflictPath = options.conflictPath;
|
||||||
|
this.existingShortcut = options.existingShortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static stringifyShortcut(shortcut: KeyboardShortcut): string {
|
||||||
|
if (shortcut.type === "SEQUENCE") {
|
||||||
|
return shortcut.strokes.map(serializeKeyStroke).join(" → ");
|
||||||
|
}
|
||||||
|
return serializeKeyStroke(shortcut.stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static formatMessage(options: ShortcutConflictErrorOptions): string {
|
||||||
|
const formatted = ShortcutConflictError.stringifyShortcut(options.shortcut);
|
||||||
|
switch (options.code) {
|
||||||
|
case ShortcutConflictCode.ACTION_ON_SEQUENCE:
|
||||||
|
return `Cannot register "${formatted}": desired action conflicts with existing sequence at "${options.conflictPath}"`;
|
||||||
|
case ShortcutConflictCode.DUPLICATE_ACTION:
|
||||||
|
return `Cannot register "${formatted}": action already registered`;
|
||||||
|
case ShortcutConflictCode.SEQUENCE_ON_ACTION:
|
||||||
|
return `Cannot register "${formatted}": desired sequence conflicts with existing action at "${options.conflictPath}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyboardShortcutContextType {
|
||||||
|
registerShortcut: (shortcut: KeyboardShortcut) => () => void;
|
||||||
|
openLegend: () => void;
|
||||||
|
openLegendKeys: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyboardShortcutContext = createContext<
|
||||||
|
KeyboardShortcutContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export function KeyboardShortcutProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const treeRootRef = useRef<ShortcutTreeLevel>({});
|
||||||
|
const currentNodeRef = useRef<ShortcutTreeLevel>(treeRootRef.current);
|
||||||
|
const shortcutsRef = useRef<Map<string, KeyboardShortcut>>(new Map());
|
||||||
|
const sequenceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const [isLegendOpen, setIsLegendOpen] = useState(false);
|
||||||
|
|
||||||
|
const openLegendShortcut: KeyboardShortcut = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: "PRESS",
|
||||||
|
stroke: {
|
||||||
|
key: "/",
|
||||||
|
modifiers: ["META"],
|
||||||
|
},
|
||||||
|
action: () => setIsLegendOpen(true),
|
||||||
|
description: t`Open keyboard shortcuts`,
|
||||||
|
group: ShortcutGroup.GENERAL,
|
||||||
|
}),
|
||||||
|
[setIsLegendOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||||
|
if (isTypingInInput(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sequenceTimeoutRef.current) {
|
||||||
|
clearTimeout(sequenceTimeoutRef.current);
|
||||||
|
sequenceTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedKey = serializeEvent(event);
|
||||||
|
const node = currentNodeRef.current[serializedKey];
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
currentNodeRef.current = treeRootRef.current;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case "STEP":
|
||||||
|
event.preventDefault();
|
||||||
|
currentNodeRef.current = node.children;
|
||||||
|
sequenceTimeoutRef.current = setTimeout(() => {
|
||||||
|
currentNodeRef.current = treeRootRef.current;
|
||||||
|
}, SEQUENCE_TIMEOUT_MS);
|
||||||
|
return;
|
||||||
|
case "ACTION":
|
||||||
|
event.preventDefault();
|
||||||
|
node.shortcut.action();
|
||||||
|
currentNodeRef.current = treeRootRef.current;
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const registerShortcut = useCallback(
|
||||||
|
(shortcut: KeyboardShortcut): (() => void) => {
|
||||||
|
const strokes =
|
||||||
|
shortcut.type === "SEQUENCE" ? shortcut.strokes : [shortcut.stroke];
|
||||||
|
|
||||||
|
const path = strokes.map(serializeKeyStroke);
|
||||||
|
const pathKey = path.join(" → ");
|
||||||
|
|
||||||
|
path.reduce((currentLevel, key, i) => {
|
||||||
|
const isLast = i === path.length - 1;
|
||||||
|
const existingNode = currentLevel[key];
|
||||||
|
|
||||||
|
if (env.NODE_ENV === "development" && existingNode) {
|
||||||
|
const conflictPath = path.slice(0, i + 1).join(" → ");
|
||||||
|
validateNoConflict(existingNode, isLast, shortcut, conflictPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLast) {
|
||||||
|
currentLevel[key] = {
|
||||||
|
type: "ACTION",
|
||||||
|
shortcut,
|
||||||
|
};
|
||||||
|
return currentLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingNode?.type === "STEP") {
|
||||||
|
return existingNode.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNode: ShortcutTreeStepNode = {
|
||||||
|
type: "STEP",
|
||||||
|
children: {},
|
||||||
|
};
|
||||||
|
currentLevel[key] = newNode;
|
||||||
|
return newNode.children;
|
||||||
|
}, treeRootRef.current);
|
||||||
|
|
||||||
|
shortcutsRef.current.set(pathKey, shortcut);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removePathAndPrune(treeRootRef.current, path);
|
||||||
|
shortcutsRef.current.delete(pathKey);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
// Register built-in shortcut to open legend
|
||||||
|
useEffect(() => {
|
||||||
|
const cleanup = registerShortcut(openLegendShortcut);
|
||||||
|
return cleanup;
|
||||||
|
}, [registerShortcut, openLegendShortcut]);
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (sequenceTimeoutRef.current) {
|
||||||
|
clearTimeout(sequenceTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const shortcutsArray = Array.from(shortcutsRef.current.values());
|
||||||
|
const groupedShortcuts = shortcutsArray.reduce<
|
||||||
|
Partial<Record<ShortcutGroup, KeyboardShortcut[]>>
|
||||||
|
>((acc, shortcut) => {
|
||||||
|
const group = shortcut.group;
|
||||||
|
acc[group] ??= [];
|
||||||
|
acc[group].push(shortcut);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const openLegend = useCallback(() => {
|
||||||
|
setIsLegendOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openLegendKeys = useMemo(
|
||||||
|
() => <FormattedShortcut shortcut={openLegendShortcut} />,
|
||||||
|
[openLegendShortcut],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardShortcutContext.Provider
|
||||||
|
value={{ registerShortcut, openLegend, openLegendKeys }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Shortcut Legend */}
|
||||||
|
<Dialog
|
||||||
|
className="relative z-50"
|
||||||
|
open={isLegendOpen}
|
||||||
|
onClose={() => setIsLegendOpen(false)}
|
||||||
|
>
|
||||||
|
<DialogBackdrop
|
||||||
|
transition
|
||||||
|
className="data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in fixed inset-0 bg-light-50 bg-opacity-40 transition-opacity dark:bg-dark-50 dark:bg-opacity-40"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 flex min-h-full w-screen items-center justify-center overflow-y-auto p-4">
|
||||||
|
<DialogPanel
|
||||||
|
transition
|
||||||
|
className="relative w-full max-w-sm transform overflow-hidden rounded-lg border border-light-600 bg-white shadow-3xl-light dark:border-dark-600 dark:bg-dark-100 dark:shadow-3xl-dark"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-light-300 px-6 py-4 dark:border-dark-300">
|
||||||
|
<DialogTitle className="text-[14px] font-semibold text-neutral-900 dark:text-dark-1000">
|
||||||
|
{t`Keyboard Shortcuts`}
|
||||||
|
</DialogTitle>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLegendOpen(false)}
|
||||||
|
className="rounded p-1 hover:bg-light-200 dark:hover:bg-dark-200"
|
||||||
|
>
|
||||||
|
<HiXMark className="h-5 w-5 text-neutral-700 dark:text-dark-700" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[60vh] overflow-y-auto p-6">
|
||||||
|
{shortcutsArray.length === 0 ? (
|
||||||
|
<p className="text-center text-sm text-neutral-600 dark:text-dark-600">
|
||||||
|
{t`No keyboard shortcuts registered.`}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.values(ShortcutGroup).map((group, idx) => {
|
||||||
|
const shortcuts = groupedShortcuts[group];
|
||||||
|
if (!shortcuts?.length) return null;
|
||||||
|
const groupInfo = getShortcutGroupInfo();
|
||||||
|
return (
|
||||||
|
<div key={`${group}-${idx}`}>
|
||||||
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-light-1000 dark:text-dark-1000">
|
||||||
|
{groupInfo[group].label}
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
{shortcuts.map((shortcut) => (
|
||||||
|
<ShortcutListItem
|
||||||
|
key={shortcut.description}
|
||||||
|
shortcut={shortcut}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</KeyboardShortcutContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to register a keyboard shortcut. Cleanup is handled automatically.
|
||||||
|
* Returns both formatted keys and pre-built tooltip content.
|
||||||
|
*
|
||||||
|
* IMPORTANT: The shortcut object reference must be stable. If it changes
|
||||||
|
* on every render, the shortcut will be continuously registered and unregistered.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const { keys, tooltipContent } = useKeyboardShortcut({
|
||||||
|
* type: "PRESS",
|
||||||
|
* stroke: { key: "k", modifiers: ["META"] },
|
||||||
|
* action: () => openCommandPalette(),
|
||||||
|
* description: "Open command palette",
|
||||||
|
* group: "GENERAL"
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Use tooltip for Tooltip component
|
||||||
|
* <Tooltip content={tooltipContent}>
|
||||||
|
* <button>Search</button>
|
||||||
|
* </Tooltip>
|
||||||
|
*
|
||||||
|
* // Or use keys directly for custom formatting
|
||||||
|
* <span>Press {keys} to search</span>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useKeyboardShortcuts(): KeyboardShortcutContextType {
|
||||||
|
const context = useContext(KeyboardShortcutContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useKeyboardShortcuts must be used within KeyboardShortcutProvider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeyboardShortcut(shortcut: KeyboardShortcut): {
|
||||||
|
keys: ReactNode;
|
||||||
|
tooltipContent: ReactNode;
|
||||||
|
} {
|
||||||
|
const context = useContext(KeyboardShortcutContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useKeyboardShortcut must be used within KeyboardShortcutProvider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { registerShortcut } = context;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cleanup = registerShortcut(shortcut);
|
||||||
|
return cleanup;
|
||||||
|
}, [shortcut, registerShortcut]);
|
||||||
|
|
||||||
|
const keys = <FormattedShortcut shortcut={shortcut} />;
|
||||||
|
const tooltipContent = (
|
||||||
|
<div className="flex flex-row items-center gap-2 text-[11px]">
|
||||||
|
{shortcut.description} {keys}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return { keys, tooltipContent };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShortcutListItem({ shortcut }: { shortcut: KeyboardShortcut }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-sm text-dark-50 dark:text-dark-900">
|
||||||
|
{shortcut.description}
|
||||||
|
</span>
|
||||||
|
<FormattedShortcut shortcut={shortcut} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormattedShortcut({ shortcut }: { shortcut: KeyboardShortcut }) {
|
||||||
|
const kbdClassName =
|
||||||
|
"inline-flex h-5 w-5 items-center justify-center rounded border border-light-400 bg-light-200 px-1.5 py-0.5 font-mono text-[8px] font-semibold text-center text-neutral-900 dark:border-dark-400 dark:bg-dark-200 dark:text-dark-950";
|
||||||
|
|
||||||
|
const stringifyModifier = (modifier: ModifierKey): string => {
|
||||||
|
const isMac =
|
||||||
|
typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
|
||||||
|
const isLinux =
|
||||||
|
typeof navigator !== "undefined" && navigator.userAgent.includes("Linux");
|
||||||
|
|
||||||
|
const info = ModifierKeyInfo[modifier];
|
||||||
|
if (isMac) return info.macSymbol;
|
||||||
|
if (isLinux) return info.linuxName;
|
||||||
|
return info.winName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatStroke = (stroke: KeyStroke): ReactNode[] => {
|
||||||
|
const parts: ReactNode[] = [];
|
||||||
|
const modifierStrings = stroke.modifiers
|
||||||
|
? stroke.modifiers.map(stringifyModifier)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
modifierStrings.forEach((mod) => {
|
||||||
|
parts.push(<kbd className={kbdClassName}>{mod}</kbd>);
|
||||||
|
});
|
||||||
|
|
||||||
|
parts.push(<kbd className={kbdClassName}>{stroke.key.toUpperCase()}</kbd>);
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shortcut.type === "SEQUENCE") {
|
||||||
|
const parts: ReactNode[] = [];
|
||||||
|
shortcut.strokes.forEach((stroke) => {
|
||||||
|
parts.push(...formatStroke(stroke));
|
||||||
|
});
|
||||||
|
return <span className="flex items-center gap-1 text-[11px]">{parts}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="inline-flex flex-shrink-0 items-center gap-1 text-[11px]">
|
||||||
|
{formatStroke(shortcut.stroke)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks for conflicts given existing nodes and current path
|
||||||
|
* Throws an error if conflict is found
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function validateNoConflict(
|
||||||
|
existingNode: ShortcutTreeNode,
|
||||||
|
isLastKey: boolean,
|
||||||
|
shortcut: KeyboardShortcut,
|
||||||
|
conflictPath: string,
|
||||||
|
): void {
|
||||||
|
if (isLastKey) {
|
||||||
|
if (existingNode.type === "STEP") {
|
||||||
|
throw new ShortcutConflictError({
|
||||||
|
code: ShortcutConflictCode.ACTION_ON_SEQUENCE,
|
||||||
|
shortcut,
|
||||||
|
conflictPath,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new ShortcutConflictError({
|
||||||
|
code: ShortcutConflictCode.DUPLICATE_ACTION,
|
||||||
|
shortcut,
|
||||||
|
conflictPath,
|
||||||
|
existingShortcut: existingNode.shortcut,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (existingNode.type === "ACTION") {
|
||||||
|
throw new ShortcutConflictError({
|
||||||
|
code: ShortcutConflictCode.SEQUENCE_ON_ACTION,
|
||||||
|
shortcut,
|
||||||
|
conflictPath,
|
||||||
|
existingShortcut: existingNode.shortcut,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user is currently typing in an input field
|
||||||
|
*/
|
||||||
|
function isTypingInInput(event: KeyboardEvent): boolean {
|
||||||
|
if (!event.target) return false;
|
||||||
|
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const tagName = target.tagName.toLowerCase();
|
||||||
|
const isInput = tagName === "input" || tagName === "textarea";
|
||||||
|
const isContentEditable = target.isContentEditable;
|
||||||
|
|
||||||
|
return isInput || isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes a KeyStroke to a consistent string format
|
||||||
|
* Returns lowercase string like "ctrl+shift+k" with alphabetically sorted modifiers
|
||||||
|
*/
|
||||||
|
function serializeKeyStroke(stroke: KeyStroke): string {
|
||||||
|
const key = stroke.key.toLowerCase();
|
||||||
|
if (!stroke.modifiers || stroke.modifiers.length === 0) return key;
|
||||||
|
const sorted = [...stroke.modifiers].sort();
|
||||||
|
return `${sorted.join("+")}+${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a keyboard event to a KeyStroke
|
||||||
|
*/
|
||||||
|
function eventToKeyStroke(event: KeyboardEvent): KeyStroke {
|
||||||
|
const modifiers: ModifierKey[] = [];
|
||||||
|
if (event.altKey) modifiers.push(ModifierKey.ALT);
|
||||||
|
if (event.ctrlKey) modifiers.push(ModifierKey.CONTROL);
|
||||||
|
if (event.metaKey) modifiers.push(ModifierKey.META);
|
||||||
|
// Only include shift if the key is a letter (shift wasn't consumed to produce the character)
|
||||||
|
if (event.shiftKey && /^[a-zA-Z]$/.test(event.key))
|
||||||
|
modifiers.push(ModifierKey.SHIFT);
|
||||||
|
return { key: event.key, modifiers };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes a keyboard event to a consistent string format
|
||||||
|
*/
|
||||||
|
function serializeEvent(event: KeyboardEvent): string {
|
||||||
|
return serializeKeyStroke(eventToKeyStroke(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a path from the tree and prunes empty branches
|
||||||
|
*/
|
||||||
|
function removePathAndPrune(tree: ShortcutTreeLevel, path: string[]): void {
|
||||||
|
const [first, ...rest] = path;
|
||||||
|
if (!first) return;
|
||||||
|
|
||||||
|
if (rest.length === 0) {
|
||||||
|
delete tree[first];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = tree[first];
|
||||||
|
if (!node || node.type !== "STEP") return;
|
||||||
|
|
||||||
|
removePathAndPrune(node.children, rest);
|
||||||
|
|
||||||
|
if (Object.keys(node.children).length === 0) {
|
||||||
|
delete tree[first];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,3 +39,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tippy.js tooltip theme */
|
||||||
|
.tippy-box[data-theme~="tooltip"] {
|
||||||
|
@apply rounded-md border border-light-600 bg-white px-2 py-1 text-sm text-neutral-900 shadow-lg dark:border-dark-600 dark:bg-dark-100 dark:text-dark-1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-box[data-theme~="tooltip"] > .tippy-arrow::before {
|
||||||
|
@apply border-t-light-600 dark:border-t-dark-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-box[data-theme~="tooltip"][data-placement^="bottom"]
|
||||||
|
> .tippy-arrow::before {
|
||||||
|
@apply border-b-light-600 dark:border-b-dark-600;
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
|||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
import PatternedBackground from "~/components/PatternedBackground";
|
import PatternedBackground from "~/components/PatternedBackground";
|
||||||
import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppable";
|
import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppable";
|
||||||
|
import { Tooltip } from "~/components/Tooltip";
|
||||||
|
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
@@ -54,6 +56,15 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
useState<PublicListId>("");
|
useState<PublicListId>("");
|
||||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
|
|
||||||
|
const { tooltipContent: createListShortcutTooltipContent } =
|
||||||
|
useKeyboardShortcut({
|
||||||
|
type: "PRESS",
|
||||||
|
stroke: { key: "C" },
|
||||||
|
action: () => boardId && openNewListForm(boardId),
|
||||||
|
description: t`Create new list`,
|
||||||
|
group: "ACTIONS",
|
||||||
|
});
|
||||||
|
|
||||||
const boardId = params?.boardId
|
const boardId = params?.boardId
|
||||||
? Array.isArray(params.boardId)
|
? Array.isArray(params.boardId)
|
||||||
? params.boardId[0]
|
? params.boardId[0]
|
||||||
@@ -427,20 +438,22 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Tooltip content={createListShortcutTooltipContent}>
|
||||||
iconLeft={
|
<Button
|
||||||
<HiOutlinePlusSmall
|
iconLeft={
|
||||||
className="-mr-0.5 h-5 w-5"
|
<HiOutlinePlusSmall
|
||||||
aria-hidden="true"
|
className="-mr-0.5 h-5 w-5"
|
||||||
/>
|
aria-hidden="true"
|
||||||
}
|
/>
|
||||||
onClick={() => {
|
}
|
||||||
if (boardId) openNewListForm(boardId);
|
onClick={() => {
|
||||||
}}
|
if (boardId) openNewListForm(boardId);
|
||||||
disabled={!boardData}
|
}}
|
||||||
>
|
disabled={!boardData}
|
||||||
{t`New list`}
|
>
|
||||||
</Button>
|
{t`New list`}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<BoardDropdown
|
<BoardDropdown
|
||||||
isTemplate={!!isTemplate}
|
isTemplate={!!isTemplate}
|
||||||
isLoading={!boardData}
|
isLoading={!boardData}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import FeedbackModal from "~/components/FeedbackModal";
|
|||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
|
import { Tooltip } from "~/components/Tooltip";
|
||||||
|
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import { BoardsList } from "./components/BoardsList";
|
import { BoardsList } from "./components/BoardsList";
|
||||||
@@ -16,6 +18,15 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
const { openModal, modalContentType, isOpen } = useModal();
|
const { openModal, modalContentType, isOpen } = useModal();
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { tooltipContent: createModalShortcutTooltipContent } =
|
||||||
|
useKeyboardShortcut({
|
||||||
|
type: "PRESS",
|
||||||
|
stroke: { key: "C" },
|
||||||
|
action: () => openModal("NEW_BOARD"),
|
||||||
|
description: t`Create new ${isTemplate ? "template" : "board"}`,
|
||||||
|
group: "ACTIONS",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead
|
<PageHead
|
||||||
@@ -39,16 +50,18 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
{t`Import`}
|
{t`Import`}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Tooltip content={createModalShortcutTooltipContent}>
|
||||||
type="button"
|
<Button
|
||||||
variant="primary"
|
type="button"
|
||||||
onClick={() => openModal("NEW_BOARD")}
|
variant="primary"
|
||||||
iconLeft={
|
onClick={() => openModal("NEW_BOARD")}
|
||||||
<HiOutlinePlusSmall aria-hidden="true" className="h-4 w-4" />
|
iconLeft={
|
||||||
}
|
<HiOutlinePlusSmall aria-hidden="true" className="h-4 w-4" />
|
||||||
>
|
}
|
||||||
{t`New`}
|
>
|
||||||
</Button>
|
{t`New`}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -67,22 +67,23 @@ export function CardModal({
|
|||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<button
|
<button
|
||||||
className="absolute right-[2rem] top-[2rem] rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
|
className="absolute right-[2rem] top-[2rem] rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
|
||||||
onClick={async (e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(() => {
|
||||||
try {
|
void router.replace(
|
||||||
await router.replace(
|
{
|
||||||
`/${workspaceSlug}/${boardSlug}`,
|
pathname: router.pathname,
|
||||||
undefined,
|
query: {
|
||||||
{
|
...router.query,
|
||||||
shallow: true,
|
workspaceSlug,
|
||||||
|
boardSlug: [boardSlug],
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
} catch (error) {
|
undefined,
|
||||||
console.error(error);
|
{ shallow: true },
|
||||||
}
|
);
|
||||||
}, 400);
|
}, 400);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ export default function PublicBoardView() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitPath = router.asPath.split("/");
|
const pathWithoutQuery = router.asPath.split("?")[0];
|
||||||
|
const splitPath = pathWithoutQuery.split("/");
|
||||||
const cardPublicId = splitPath.length > 3 ? splitPath[3] : null;
|
const cardPublicId = splitPath.length > 3 ? splitPath[3] : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -172,27 +173,36 @@ export default function PublicBoardView() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] z-10 h-full max-h-[calc(100vh-265px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600">
|
<div className="scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] z-10 h-full max-h-[calc(100vh-265px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600">
|
||||||
{list.cards.map((card) => (
|
{list.cards.map((card) => {
|
||||||
<Link
|
return (
|
||||||
key={card.publicId}
|
<Link
|
||||||
href={`/${data.workspace.slug}/${data.slug}/${card.publicId}`}
|
key={card.publicId}
|
||||||
className={`mb-2 flex !cursor-pointer flex-col`}
|
href={{
|
||||||
shallow={true}
|
pathname: router.pathname,
|
||||||
onClick={() => {
|
query: {
|
||||||
openModal("CARD");
|
...router.query,
|
||||||
}}
|
workspaceSlug: data.workspace.slug,
|
||||||
>
|
boardSlug: [data.slug, card.publicId],
|
||||||
<Card
|
},
|
||||||
title={card.title}
|
}}
|
||||||
labels={card.labels}
|
className={`mb-2 flex !cursor-pointer flex-col`}
|
||||||
checklists={card.checklists ?? []}
|
shallow={true}
|
||||||
members={[]}
|
onClick={() => {
|
||||||
description={card.description}
|
openModal("CARD");
|
||||||
comments={card.comments ?? []}
|
}}
|
||||||
attachments={card.attachments}
|
>
|
||||||
/>
|
<Card
|
||||||
</Link>
|
title={card.title}
|
||||||
))}
|
labels={card.labels}
|
||||||
|
checklists={card.checklists ?? []}
|
||||||
|
members={[]}
|
||||||
|
description={card.description}
|
||||||
|
comments={card.comments ?? []}
|
||||||
|
attachments={card.attachments}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ services:
|
|||||||
# Notifications
|
# Notifications
|
||||||
- NOVU_API_KEY=${NOVU_API_KEY}
|
- NOVU_API_KEY=${NOVU_API_KEY}
|
||||||
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
|
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
|
||||||
|
- EMAIL_UNSUBSCRIBE_SECRET=${EMAIL_UNSUBSCRIBE_SECRET}
|
||||||
|
|
||||||
# S3 storage
|
# S3 storage
|
||||||
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
|
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { env } from "next-runtime-env";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import * as inviteLinkRepo from "@kan/db/repository/inviteLink.repo";
|
import * as inviteLinkRepo from "@kan/db/repository/inviteLink.repo";
|
||||||
@@ -307,7 +308,7 @@ export const memberRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
id: activeInviteLink.id,
|
id: activeInviteLink.id,
|
||||||
inviteCode: activeInviteLink.code,
|
inviteCode: activeInviteLink.code,
|
||||||
inviteLink: `${process.env.NEXT_PUBLIC_BASE_URL}/invite/${activeInviteLink.code}`,
|
inviteLink: `${env("NEXT_PUBLIC_BASE_URL")}/invite/${activeInviteLink.code}`,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
expiresAt: activeInviteLink.expiresAt ?? undefined,
|
expiresAt: activeInviteLink.expiresAt ?? undefined,
|
||||||
};
|
};
|
||||||
@@ -415,7 +416,7 @@ export const memberRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
publicId: inviteLink.publicId,
|
publicId: inviteLink.publicId,
|
||||||
inviteCode: inviteLink.code,
|
inviteCode: inviteLink.code,
|
||||||
inviteLink: `${process.env.NEXT_PUBLIC_BASE_URL}/invite/${inviteLink.code}`,
|
inviteLink: `${env("NEXT_PUBLIC_BASE_URL")}/invite/${inviteLink.code}`,
|
||||||
expiresAt: inviteLink.expiresAt,
|
expiresAt: inviteLink.expiresAt,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -476,7 +477,7 @@ export const memberRouter = createTRPCRouter({
|
|||||||
openapi: {
|
openapi: {
|
||||||
summary: "Get invite information by code",
|
summary: "Get invite information by code",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/workspaces/{workspacePublicId}/invites/{inviteCode}",
|
path: "/invites/{inviteCode}",
|
||||||
description: "Get invite information by invite code",
|
description: "Get invite information by invite code",
|
||||||
tags: ["Invites"],
|
tags: ["Invites"],
|
||||||
protect: false,
|
protect: false,
|
||||||
@@ -521,7 +522,7 @@ export const memberRouter = createTRPCRouter({
|
|||||||
openapi: {
|
openapi: {
|
||||||
summary: "Accept an invite link",
|
summary: "Accept an invite link",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/workspaces/{workspacePublicId}/invites/accept",
|
path: "/invites/accept",
|
||||||
description: "Accepts an invitation via invite link",
|
description: "Accepts an invitation via invite link",
|
||||||
tags: ["Invites"],
|
tags: ["Invites"],
|
||||||
protect: false,
|
protect: false,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import * as userRepo from "@kan/db/repository/user.repo";
|
|||||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
import * as schema from "@kan/db/schema";
|
import * as schema from "@kan/db/schema";
|
||||||
import { notificationClient, sendEmail } from "@kan/email";
|
import { notificationClient, sendEmail } from "@kan/email";
|
||||||
|
import { createEmailUnsubscribeLink } from "@kan/shared";
|
||||||
import { createStripeClient } from "@kan/stripe";
|
import { createStripeClient } from "@kan/stripe";
|
||||||
|
|
||||||
export const configuredProviders = socialProviderList.reduce<
|
export const configuredProviders = socialProviderList.reduce<
|
||||||
@@ -401,6 +402,10 @@ export const initAuth = (db: dbClient) => {
|
|||||||
? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}`
|
? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const unsubscribeUrl = await createEmailUnsubscribeLink(
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
|
||||||
await notificationClient.trigger({
|
await notificationClient.trigger({
|
||||||
to: {
|
to: {
|
||||||
subscriberId: user.id,
|
subscriberId: user.id,
|
||||||
@@ -415,6 +420,9 @@ export const initAuth = (db: dbClient) => {
|
|||||||
updatedAt: user.updatedAt,
|
updatedAt: user.updatedAt,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
payload: {
|
||||||
|
emailUnsubscribeUrl: unsubscribeUrl,
|
||||||
|
},
|
||||||
workflowId: "user-signup",
|
workflowId: "user-signup",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -489,6 +497,8 @@ async function triggerWorkflow(
|
|||||||
|
|
||||||
if (!user || !notificationClient) return;
|
if (!user || !notificationClient) return;
|
||||||
|
|
||||||
|
const unsubscribeUrl = await createEmailUnsubscribeLink(user.id);
|
||||||
|
|
||||||
await notificationClient.trigger({
|
await notificationClient.trigger({
|
||||||
to: {
|
to: {
|
||||||
subscriberId: user.id,
|
subscriberId: user.id,
|
||||||
@@ -496,6 +506,7 @@ async function triggerWorkflow(
|
|||||||
payload: {
|
payload: {
|
||||||
...subscription,
|
...subscription,
|
||||||
cancellationDetails,
|
cancellationDetails,
|
||||||
|
emailUnsubscribeUrl: unsubscribeUrl,
|
||||||
},
|
},
|
||||||
workflowId,
|
workflowId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@kan/prettier-config",
|
"prettier": "@kan/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^5.0.9"
|
"jose": "^6.1.2",
|
||||||
|
"nanoid": "^5.0.9",
|
||||||
|
"next-runtime-env": "^1.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
packages/shared/src/utils/email.ts
Normal file
32
packages/shared/src/utils/email.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { SignJWT } from "jose";
|
||||||
|
import { env } from "next-runtime-env";
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a long‑lived unsubscribe link for a given user/subscriber.
|
||||||
|
*
|
||||||
|
* `${NEXT_PUBLIC_BASE_URL}/unsubscribe?token=<jwt>`
|
||||||
|
*
|
||||||
|
* The JWT payload only contains the subscriberId. There is no expiry
|
||||||
|
* on purpose – unsubscribe links should remain valid indefinitely.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export async function createEmailUnsubscribeLink(
|
||||||
|
userId: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const baseUrl = env("NEXT_PUBLIC_BASE_URL");
|
||||||
|
const secret = process.env.EMAIL_UNSUBSCRIBE_SECRET;
|
||||||
|
|
||||||
|
if (!baseUrl || !secret) {
|
||||||
|
// Environment not configured for unsubscribe links.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await new SignJWT({ subscriberId: userId })
|
||||||
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
|
// No expiration on purpose; unsubscribe links are long‑lived.
|
||||||
|
.sign(encoder.encode(secret));
|
||||||
|
|
||||||
|
return `${baseUrl}/unsubscribe?token=${encodeURIComponent(token)}`;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./generateUID";
|
export * from "./generateUID";
|
||||||
export * from "./generateSlug";
|
export * from "./generateSlug";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
|
export * from "./email";
|
||||||
|
|||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -115,6 +115,9 @@ importers:
|
|||||||
'@lingui/react':
|
'@lingui/react':
|
||||||
specifier: ^5.3.2
|
specifier: ^5.3.2
|
||||||
version: 5.4.1(@lingui/babel-plugin-lingui-macro@5.4.1(typescript@5.9.2))(react@18.3.1)
|
version: 5.4.1(@lingui/babel-plugin-lingui-macro@5.4.1(typescript@5.9.2))(react@18.3.1)
|
||||||
|
'@novu/api':
|
||||||
|
specifier: ^3.11.0
|
||||||
|
version: 3.11.0
|
||||||
'@t3-oss/env-nextjs':
|
'@t3-oss/env-nextjs':
|
||||||
specifier: ^0.11.1
|
specifier: ^0.11.1
|
||||||
version: 0.11.1(typescript@5.9.2)(zod@3.25.76)
|
version: 0.11.1(typescript@5.9.2)(zod@3.25.76)
|
||||||
@@ -166,6 +169,9 @@ importers:
|
|||||||
geist:
|
geist:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.4.2(next@15.5.2(@babel/core@7.28.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
version: 1.4.2(next@15.5.2(@babel/core@7.28.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||||
|
jose:
|
||||||
|
specifier: ^6.1.2
|
||||||
|
version: 6.1.2
|
||||||
next:
|
next:
|
||||||
specifier: ^15.3.4
|
specifier: ^15.3.4
|
||||||
version: 15.5.2(@babel/core@7.28.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 15.5.2(@babel/core@7.28.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -477,9 +483,15 @@ importers:
|
|||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
jose:
|
||||||
|
specifier: ^6.1.2
|
||||||
|
version: 6.1.2
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.0.9
|
specifier: ^5.0.9
|
||||||
version: 5.1.5
|
version: 5.1.5
|
||||||
|
next-runtime-env:
|
||||||
|
specifier: ^1.7.2
|
||||||
|
version: 1.8.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@kan/eslint-config':
|
'@kan/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -5554,6 +5566,9 @@ packages:
|
|||||||
jose@5.10.0:
|
jose@5.10.0:
|
||||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||||
|
|
||||||
|
jose@6.1.2:
|
||||||
|
resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
|
||||||
|
|
||||||
joycon@3.1.1:
|
joycon@3.1.1:
|
||||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -13649,6 +13664,8 @@ snapshots:
|
|||||||
|
|
||||||
jose@5.10.0: {}
|
jose@5.10.0: {}
|
||||||
|
|
||||||
|
jose@6.1.2: {}
|
||||||
|
|
||||||
joycon@3.1.1: {}
|
joycon@3.1.1: {}
|
||||||
|
|
||||||
js-beautify@1.15.4:
|
js-beautify@1.15.4:
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
"PORT",
|
"PORT",
|
||||||
"BETTER_AUTH_SECRET",
|
"BETTER_AUTH_SECRET",
|
||||||
"BETTER_AUTH_TRUSTED_ORIGINS",
|
"BETTER_AUTH_TRUSTED_ORIGINS",
|
||||||
"NOVU_API_KEY"
|
"NOVU_API_KEY",
|
||||||
|
"EMAIL_UNSUBSCRIBE_SECRET"
|
||||||
],
|
],
|
||||||
"globalPassThroughEnv": [
|
"globalPassThroughEnv": [
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
|
|||||||
Reference in New Issue
Block a user