Compare commits
20 Commits
v0.4.8
...
fix/kan-24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcb28a3429 | ||
|
|
f938723d9a | ||
|
|
0928709173 | ||
|
|
ade9aad4bf | ||
|
|
5fbaf26707 | ||
|
|
b69835f7de | ||
|
|
58ff6ad041 | ||
|
|
adbc945ed6 | ||
|
|
c5d85cdb2d | ||
|
|
56e4c43cfa | ||
|
|
07fc41761b | ||
|
|
50382e9186 | ||
|
|
00be5fb058 | ||
|
|
fc65f53e0c | ||
|
|
64aace6e3b | ||
|
|
1651faae14 | ||
|
|
b472c5ce93 | ||
|
|
af6490896a | ||
|
|
c0a3c37d95 | ||
|
|
c30da618ec |
@@ -29,6 +29,7 @@ S3_SECRET_ACCESS_KEY=
|
||||
S3_FORCE_PATH_STYLE=
|
||||
NEXT_PUBLIC_STORAGE_URL=
|
||||
NEXT_PUBLIC_AVATAR_BUCKET_NAME=
|
||||
NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME=
|
||||
NEXT_PUBLIC_STORAGE_DOMAIN=
|
||||
|
||||
# Auth config (optional)
|
||||
|
||||
@@ -170,6 +170,7 @@ pnpm dev
|
||||
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
|
||||
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
|
||||
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
|
||||
| `NEXT_PUBLIC_ATTATCHMENTS_BUCKET_NAME` | S3 bucket name for attatchments | For file uploads | `attatchments` |
|
||||
| `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` |
|
||||
| `NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY` | Hide “Powered by kan.bn” on public boards (self-host) | For white labelling | `true` |
|
||||
|
||||
@@ -60,6 +60,7 @@ checksums:
|
||||
Are%20you%20sure%20you%20want%20to%20delete%20your%20account%3F/singular: 428b74d781063224c3e63980e1094ec3
|
||||
assigned%20%3C0%3E%7B0%7D%3C%2F0%3E%20to%20the%20card/singular: 9df6d89fa83706123dedd8cba2243d04
|
||||
Assignees/singular: 96fc0e28a783d80decfd4716a440ac08
|
||||
Attachment%20uploaded/singular: c0233f8c2560f95ac0834f35eb07b9e7
|
||||
Awaiting%20Customer/singular: 122c108794f321c10e05a08f09b11457
|
||||
Backlog/singular: 35919fd5b10ad212839a75e72829fe4e
|
||||
Basic%20Kanban/singular: e46ebc406749486f3907436970d4d6c7
|
||||
@@ -108,6 +109,7 @@ checksums:
|
||||
Complete%20control%20and%20ownership%3A/singular: 0d8b682ba873272217425ccfc96aa9cd
|
||||
completed%20a%20checklist%20item/singular: 757b04c6c80cc927e1c597c0ad4fda33
|
||||
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
|
||||
Connect%20Trello/singular: 4440a0b9e387ef7136e3958e7a089213
|
||||
Connect%20your%20favorite%20tools%20to%20streamline%20your%20workflow./singular: 033c5dcdb059ccb634aef7fe63f2f45d
|
||||
@@ -166,11 +168,13 @@ checksums:
|
||||
Display%20name%20must%20be%20at%20least%203%20characters%20long/singular: 84178b79845a70937af5e26020403052
|
||||
Display%20name%20updated/singular: 4e27503ca24ef5f009713951ca9e5b97
|
||||
Do%20you%20offer%20a%20free%20plan%3F/singular: 3ef16b0994f0d9ae17067782ca9bed1b
|
||||
Do%20you%20want%20to%20unsubscribe%3F/singular: 8173b82c731ba885939d223eeef2e40b
|
||||
docs/singular: 307bff5678ada918da1f1ec3edc76cac
|
||||
Docs/singular: 55fea190a0c0fb32acd3dc986cd3cc91
|
||||
Documentation/singular: 1563fcb5ddb5037b0709ccd3dd384a92
|
||||
Don't%20have%20an%20account%3F%20%3C0%3E%3C1%3ESign%20up%3C%2F1%3E%3C%2F0%3E/singular: 6ae8282ab8b4a01b4ce87b119e13714b
|
||||
Done/singular: ffd408fa29d5bc9039ef8ea1b9b699bb
|
||||
Download%20failed/singular: 142b391cca3e05f2af9617092f7358c5
|
||||
Edit%20board%20URL/singular: d8276dfc0189f371ec7d80a2047c07aa
|
||||
Edit%20comment/singular: 7e4b46525fcb6b47b71798e31c46e374
|
||||
Edit%20label/singular: 0309e0be1512b1e0b0ceb87c69a53d03
|
||||
@@ -209,6 +213,7 @@ checksums:
|
||||
Failed%20to%20copy%20invite%20link/singular: 635884d5ed8d6ee20b85a003939b4ae7
|
||||
Failed%20to%20create%20board/singular: a746e2afe881c3bf0a8e82a931ca3495
|
||||
Failed%20to%20login%20with%20%7B0%7D.%20Please%20try%20again./singular: 669a4b4247a73f53fb9b8b16e42d166f
|
||||
Failed%20to%20upload%20attachment.%20Please%20try%20again./singular: f8a50d1c8491404f73d3cf701e11f297
|
||||
FAQ/singular: 47e0ee2eb40b4e7e732e05e2233fc71c
|
||||
FAQs/singular: dc36d7992372ccd419b39ef51cf5b16c
|
||||
Fast%20%26%20lightweight/singular: 4f2a80c555a5427b94eef5124aa10775
|
||||
@@ -295,6 +300,7 @@ checksums:
|
||||
Link%20copied/singular: 4a4805700b31ec65bcb3ca962bccfd1b
|
||||
List/singular: 94f13e7ef909a4de9db7abaa1f9f0b61
|
||||
List%20name/singular: e925e2e6ccaf0eb4064a888aaea8d3c2
|
||||
Lists/singular: 9f4a73afc8de321175d71935134ef066
|
||||
Login%20%7C%20kan.bn/singular: 42a6c8dcd73e0d46e652646dc86871eb
|
||||
Logout/singular: 07948fdf20705e04a7bf68ab197512bf
|
||||
Long-term/singular: 51cb6f0b112250c5a091ca5f0866904b
|
||||
@@ -329,8 +335,10 @@ checksums:
|
||||
Newsletter/singular: dcfb9666301924431ac8ae88e5b6f9d7
|
||||
Next%20Steps/singular: 12392fe73ae60b6e5a9d851256b8477e
|
||||
No%20%7B0%7D/singular: a7a95e1dfdb4396da9e2be0e27f42323
|
||||
No%20authentication%20methods%20are%20currently%20available/singular: f718c896810da3612202887f9a4374fa
|
||||
No%20boards%20found/singular: e044088d2c51b6bdf660fafa86ee1e17
|
||||
No%20credit%20card%20required/singular: 2090aa4171dc0b60735069f89b592883
|
||||
No%20download%20URL%20available%20for%20this%20attachment./singular: e367d39420b2242f9d2fd749c87f446a
|
||||
No%20lists/singular: cedf633d99c77ff4356e089f2d98c0a6
|
||||
No%20results%20found%20for%20%22%7BdebouncedQuery%7D%22./singular: 5db6294712528cd897b15ae36f4fd834
|
||||
Offer/singular: 82b4e0c9a3f5b4bd93590847de7c32a1
|
||||
@@ -476,6 +484,7 @@ checksums:
|
||||
Unable%20to%20create%20list/singular: 7fbbf8314f8d08a4123c7daef09fed05
|
||||
Unable%20to%20create%20template/singular: 7495f1434c9188849bffd015811d5337
|
||||
Unable%20to%20create%20workspace/singular: c1c949477e121d73ed6683eddf9cd874
|
||||
Unable%20to%20delete%20attachment/singular: 99d0b17b78d44e15f13d876d155f0efc
|
||||
Unable%20to%20delete%20card/singular: 50ff06aa1265fe464f776aba33f1bef9
|
||||
Unable%20to%20delete%20checklist/singular: d7067e2c009f4919f0cca77df3c228ee
|
||||
Unable%20to%20delete%20checklist%20item/singular: a74144593b625e4d8d7ecf734462c60d
|
||||
@@ -501,6 +510,7 @@ checksums:
|
||||
Unlimited%20lists/singular: 5c418701b27b9acbc5dd199719007362
|
||||
Unlimited%20members/singular: f2949ca2dc18b0063af92e60779abb65
|
||||
Unlimited%20workspaces/singular: b504d01997bf908af1d486d9942fed36
|
||||
Unsubscribe/singular: d1a0b07295687d1f08d4df1c411072dd
|
||||
Update/singular: 079fc039262fd31b10532929685c2d1b
|
||||
Update%20label/singular: 97880b503dfe956941c5f23025a1c102
|
||||
updated%20a%20checklist%20item/singular: 198e1d1d503f4b69b752dd84d0cc0e9d
|
||||
@@ -509,6 +519,7 @@ checksums:
|
||||
updated%20the%20title%20to%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: aca196d729401d8adc5c8ca9d558840e
|
||||
Upgrade%20to%20Pro/singular: 972773025e763ddf53273df6d37a729a
|
||||
Upgrade%20to%20Pro%20(%2429%2Fmonth)/singular: 9a91ea70f5eb55aac96640bf0fd7feff
|
||||
Upload%20failed/singular: 5b7b755f05330164dbfb00096c500ee3
|
||||
Urgent/singular: e79afcd4398e52e10e7e8243a7eecabe
|
||||
URL%20can%20only%20contain%20letters%2C%20numbers%2C%20and%20hyphens/singular: 875d37d2d920f98ef60a314eda8a5d9a
|
||||
URL%20cannot%20exceed%2024%20characters/singular: 395c25732b84ac1f5b63ee310da08e93
|
||||
@@ -525,6 +536,7 @@ checksums:
|
||||
View%20workspace/singular: c9e4b789330b08c18ca8db7044409e5f
|
||||
Visibility/singular: 1ff412f8fcde0f460acbf11153649646
|
||||
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
|
||||
Welcome%20back/singular: 4928884739ba559e6e4b960e80fe1452
|
||||
What%20license%20are%20you%20using%3F/singular: 59d00e1bf28edc21b77fec94875f57ca
|
||||
@@ -557,16 +569,19 @@ checksums:
|
||||
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%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've%20been%20invited%20to%20join%20a%20workspace%20on%20kan.bn./singular: 257b840726f972f384243a72767f880f
|
||||
You've%20been%20invited%20to%20join%20a%20workspace./singular: 24fc6cdc8740f37a83df85f582f03293
|
||||
Your%20account%20has%20been%20deleted./singular: 8c8d944e07388c5877effdb2c2803dcf
|
||||
Your%20boards%20have%20been%20imported./singular: 403972e7a25afc2415762c1c2b1ec868
|
||||
Your%20display%20name%20has%20been%20updated./singular: 15e5fff36c554c16ec5214427fae1bf4
|
||||
Your%20file%20has%20been%20uploaded%20successfully./singular: 19d84e934877efc0fef285ff3d6e7849
|
||||
Your%20password%20has%20been%20changed./singular: 8f730f68d52b7efa727aa0784282ebcb
|
||||
Your%20profile%20image%20has%20been%20updated./singular: 099e1780348cd7bf617721344371a430
|
||||
Your%20Trello%20account%20has%20been%20disconnected./singular: 126aeeb73f7e4cad338056e4c933f39b
|
||||
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%20has%20been%20deleted./singular: e7a3efcfc7dd18cb3e917acb67498292
|
||||
Your%20workspace%20name%20has%20been%20updated./singular: a87ea3b0d71e6dc5dd525d77114a9322
|
||||
|
||||
@@ -30,23 +30,50 @@ const config = {
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname:
|
||||
env("S3_FORCE_PATH_STYLE") === "true"
|
||||
? `${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`
|
||||
: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`,
|
||||
},
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "localhost",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "*.googleusercontent.com",
|
||||
},
|
||||
],
|
||||
remotePatterns: (() => {
|
||||
/** @type {Array<{protocol: "http" | "https", hostname: string}>} */
|
||||
const patterns = [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname:
|
||||
env("S3_FORCE_PATH_STYLE") === "true"
|
||||
? `${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`
|
||||
: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`,
|
||||
},
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "localhost",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "*.googleusercontent.com",
|
||||
},
|
||||
];
|
||||
|
||||
// Extract root domain from S3_ENDPOINT and add wildcard pattern
|
||||
const s3Endpoint = env("S3_ENDPOINT");
|
||||
if (s3Endpoint) {
|
||||
try {
|
||||
const url = new URL(s3Endpoint);
|
||||
const hostname = url.hostname;
|
||||
const protocol = url.protocol.replace(":", "");
|
||||
|
||||
// Extract root domain (last 2 parts: e.g. cloudflarestorage.com)
|
||||
const parts = hostname.split(".");
|
||||
if (parts.length >= 2) {
|
||||
const rootDomain = parts.slice(-2).join(".");
|
||||
patterns.push({
|
||||
protocol: protocol === "http" ? "http" : "https",
|
||||
hostname: `*.${rootDomain}`,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// If S3_ENDPOINT is not a valid URL, ignore it
|
||||
}
|
||||
}
|
||||
|
||||
return patterns;
|
||||
})(),
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@lingui/conf": "^5.3.2",
|
||||
"@lingui/macro": "^5.3.2",
|
||||
"@lingui/react": "^5.3.2",
|
||||
"@novu/api": "^3.11.0",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
@@ -46,6 +47,7 @@
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"geist": "^1.3.1",
|
||||
"jose": "^6.1.2",
|
||||
"next": "^15.3.4",
|
||||
"next-logger": "^5.0.1",
|
||||
"next-runtime-env": "^1.7.2",
|
||||
@@ -65,6 +67,7 @@
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -303,6 +303,10 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
||||
|
||||
const password = watch("password");
|
||||
|
||||
const isMagicLinkAvailable = useMemo(() => {
|
||||
return isCloudEnv || (isEmailSendingEnabled && !isSignUp);
|
||||
}, [isCloudEnv, isEmailSendingEnabled, isSignUp]);
|
||||
|
||||
// Determine if we should operate in magic link mode for current form state (login only)
|
||||
const isMagicLinkMode = useMemo(() => {
|
||||
// Magic link only viable when email sending enabled AND not sign up.
|
||||
@@ -358,7 +362,16 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{socialProviders?.length !== 0 && (
|
||||
{!isCredentialsEnabled && socialProviders?.length === 0 && (
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<div className="h-[1px] w-1/3 bg-light-600 dark:bg-dark-600" />
|
||||
<span className="text-center text-sm text-light-900 dark:text-dark-900">
|
||||
{t`No authentication methods are currently available`}
|
||||
</span>
|
||||
<div className="h-[1px] w-1/3 bg-light-600 dark:bg-dark-600" />
|
||||
</div>
|
||||
)}
|
||||
{!isCredentialsEnabled && socialProviders?.length !== 0 && (
|
||||
<div className="mb-[1.5rem] flex w-full items-center gap-4">
|
||||
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
|
||||
<span className="text-sm text-light-900 dark:text-dark-900">
|
||||
@@ -381,46 +394,54 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Input
|
||||
{...register("email", { required: true })}
|
||||
placeholder={t`Enter your email address`}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-2 text-xs text-red-400">
|
||||
{t`Please enter a valid email address`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isCredentialsEnabled && (
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
{...register("password", { required: true })}
|
||||
placeholder={t`Enter your password`}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="mt-2 text-xs text-red-400">
|
||||
{errors.password.message ?? t`Please enter a valid password`}
|
||||
</p>
|
||||
{(isCredentialsEnabled || isMagicLinkAvailable) && (
|
||||
<>
|
||||
<div>
|
||||
<Input
|
||||
{...register("email", { required: true })}
|
||||
placeholder={t`Enter your email address`}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-2 text-xs text-red-400">
|
||||
{t`Please enter a valid email address`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCredentialsEnabled && (
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
{...register("password", { required: true })}
|
||||
placeholder={t`Enter your password`}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="mt-2 text-xs text-red-400">
|
||||
{errors.password.message ??
|
||||
t`Please enter a valid password`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{loginError && (
|
||||
<p className="mt-2 text-xs text-red-400">{loginError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-[1.5rem] flex items-center gap-4">
|
||||
<Button
|
||||
isLoading={isLoginWithEmailPending}
|
||||
fullWidth
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
>
|
||||
{isSignUp ? t`Sign up with ` : t`Continue with `}
|
||||
{isMagicLinkMode ? t`magic link` : t`email`}
|
||||
</Button>
|
||||
</div>
|
||||
{(isCredentialsEnabled || isMagicLinkAvailable) && (
|
||||
<div className="mt-[1.5rem] flex items-center gap-4">
|
||||
<Button
|
||||
isLoading={isLoginWithEmailPending}
|
||||
fullWidth
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
>
|
||||
{isSignUp ? t`Sign up with ` : t`Continue with `}
|
||||
{isMagicLinkMode ? t`magic link` : t`email`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Instance as TippyInstance } from "tippy.js";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import {
|
||||
BubbleMenu,
|
||||
@@ -39,9 +40,10 @@ import {
|
||||
} from "react-icons/hi2";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import tippy from "tippy.js";
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import Avatar from "./Avatar";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
|
||||
import { getAvatarUrl } from "~/utils/helpers";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
@@ -77,7 +79,7 @@ export interface RenderSuggestionsProps {
|
||||
command: (item: SlashCommandItem) => void;
|
||||
}
|
||||
|
||||
export type WorkspaceMember = {
|
||||
export interface WorkspaceMember {
|
||||
publicId: string;
|
||||
user: {
|
||||
id: string;
|
||||
@@ -85,7 +87,7 @@ export type WorkspaceMember = {
|
||||
image: string | null;
|
||||
} | null;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
const CommandsList = forwardRef<
|
||||
{ onKeyDown: (props: SuggestionKeyDownProps) => boolean },
|
||||
@@ -200,7 +202,11 @@ const RenderSuggestions = () => {
|
||||
};
|
||||
};
|
||||
|
||||
type MentionItem = { id: string; label: string; image: string | null };
|
||||
interface MentionItem {
|
||||
id: string;
|
||||
label: string;
|
||||
image: string | null;
|
||||
}
|
||||
|
||||
const MentionList = forwardRef<
|
||||
{ onKeyDown: (props: SuggestionKeyDownProps) => boolean },
|
||||
@@ -238,30 +244,32 @@ const MentionList = forwardRef<
|
||||
return (
|
||||
<div className="w-56 rounded-md border-[1px] border-light-200 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
||||
<div className="max-h-[350px] overflow-y-auto p-1 scrollbar-thin scrollbar-thumb-light-200 dark:scrollbar-thumb-dark-300">
|
||||
{items.length > 0 ? items.map((item, index) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => command(item)}
|
||||
className={twMerge(
|
||||
"group flex w-full items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300",
|
||||
index === selectedIndex && "bg-light-200 dark:bg-dark-300",
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
size="xs"
|
||||
name={item.label}
|
||||
imageUrl={
|
||||
item.image ? getAvatarUrl(item.image) : undefined
|
||||
}
|
||||
email={item.label}
|
||||
/>
|
||||
<span className="ml-3 text-[12px] font-medium text-dark-900 dark:text-dark-1000">
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
)) : (
|
||||
{items.length > 0 ? (
|
||||
items.map((item, index) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => command(item)}
|
||||
className={twMerge(
|
||||
"group flex w-full items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300",
|
||||
index === selectedIndex && "bg-light-200 dark:bg-dark-300",
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
size="xs"
|
||||
name={item.label}
|
||||
imageUrl={item.image ? getAvatarUrl(item.image) : undefined}
|
||||
email={item.label}
|
||||
/>
|
||||
<span className="ml-3 text-[12px] font-medium text-dark-900 dark:text-dark-1000">
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-start p-2">
|
||||
<span className="text-dark-900 text-[12px] dark:text-dark-1000">No results</span>
|
||||
<span className="text-[12px] text-dark-900 dark:text-dark-1000">
|
||||
No results
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -436,6 +444,7 @@ export default function Editor({
|
||||
{
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Markdown,
|
||||
Placeholder.configure({
|
||||
placeholder: readOnly
|
||||
? ""
|
||||
@@ -462,35 +471,37 @@ export default function Editor({
|
||||
}),
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
class: "mention",
|
||||
},
|
||||
suggestion: {
|
||||
char: "@",
|
||||
items: ({ query }: { query: string }) => {
|
||||
const all: MentionItem[] = workspaceMembers.map((member: WorkspaceMember) => ({
|
||||
id: member.publicId,
|
||||
label: member?.user?.name ?? member.email,
|
||||
image: member?.user?.image ?? null,
|
||||
}));
|
||||
const all: MentionItem[] = workspaceMembers.map(
|
||||
(member: WorkspaceMember) => ({
|
||||
id: member.publicId,
|
||||
label: member?.user?.name ?? member.email,
|
||||
image: member?.user?.image ?? null,
|
||||
}),
|
||||
);
|
||||
const q = query.toLowerCase();
|
||||
return all.filter((u) => u.label.toLowerCase().includes(q));
|
||||
},
|
||||
command: ({ editor, range, props }: any) => {
|
||||
const mentionHTML = `<span data-type="mention" data-id="${props.id}" data-label="${props.label}">@${props.label}</span> `;
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertContent(mentionHTML)
|
||||
.focus()
|
||||
.run();
|
||||
},
|
||||
render: renderMentionSuggestions,
|
||||
},
|
||||
renderText({ options, node }) {
|
||||
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
|
||||
const mentionHTML = `<span data-type="mention" data-id="${props.id}" data-label="${props.label}">@${props.label}</span> `;
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertContent(mentionHTML)
|
||||
.focus()
|
||||
.run();
|
||||
},
|
||||
render: renderMentionSuggestions,
|
||||
},
|
||||
renderText({ options, node }) {
|
||||
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
|
||||
},
|
||||
}),
|
||||
],
|
||||
content,
|
||||
|
||||
@@ -2,8 +2,10 @@ import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import type { KeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||
import LottieIcon from "~/components/LottieIcon";
|
||||
import { useIsMobile } from "~/hooks/useMediaQuery";
|
||||
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||
|
||||
const Button: React.FC<{
|
||||
href: string;
|
||||
@@ -12,10 +14,20 @@ const Button: React.FC<{
|
||||
json: object;
|
||||
isCollapsed?: boolean;
|
||||
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 [index, setIndex] = useState(0);
|
||||
const isMobile = useIsMobile();
|
||||
const { keys: shortcutKeys } = useKeyboardShortcut(keyboardShortcut);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
@@ -34,18 +46,27 @@ const Button: React.FC<{
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onClick={handleClick}
|
||||
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
|
||||
? "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",
|
||||
isCollapsed
|
||||
? "justify-start gap-x-3 md:justify-center md:gap-x-0"
|
||||
: "gap-x-3",
|
||||
)}
|
||||
title={isCollapsed ? name : undefined}
|
||||
>
|
||||
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
||||
<span className={twMerge(isCollapsed && "md:hidden")}>{name}</span>
|
||||
<div
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import type { Subscription } 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 boardsIconLight from "~/assets/boards-light.json";
|
||||
import membersIconDark from "~/assets/members-dark.json";
|
||||
@@ -86,26 +87,59 @@ export default function SideNavigation({
|
||||
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
const navigation = [
|
||||
const navigation: {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: object;
|
||||
keyboardShortcut: KeyboardShortcut;
|
||||
}[] = [
|
||||
{
|
||||
name: t`Boards`,
|
||||
href: "/boards",
|
||||
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`,
|
||||
href: "/templates",
|
||||
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`,
|
||||
href: "/members",
|
||||
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`,
|
||||
href: "/settings",
|
||||
icon: isDarkMode ? settingsIconDark : settingsIconLight,
|
||||
keyboardShortcut: {
|
||||
type: "SEQUENCE",
|
||||
strokes: [{ key: "G" }, { key: "S" }],
|
||||
action: () => router.push("/settings"),
|
||||
group: "NAVIGATION",
|
||||
description: t`Go to settings`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -122,10 +156,10 @@ export default function SideNavigation({
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="hidden h-14 items-center justify-between pb-[18px] pt-1.5 md:flex">
|
||||
<div className="hidden h-[45px] items-center justify-between pb-3 md:flex">
|
||||
{!isCollapsed && (
|
||||
<Link href="/" className="block">
|
||||
<h1 className="pl-2 text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
||||
<h1 className="pl-2 text-[16px] font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
||||
kan.bn
|
||||
</h1>
|
||||
</Link>
|
||||
@@ -163,6 +197,7 @@ export default function SideNavigation({
|
||||
json={item.icon}
|
||||
isCollapsed={isCollapsed}
|
||||
onCloseSideNav={onCloseSideNav}
|
||||
keyboardShortcut={item.keyboardShortcut}
|
||||
/>
|
||||
</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 { useIsMobile } from "~/hooks/useMediaQuery";
|
||||
import { useKeyboardShortcuts } from "~/providers/keyboard-shortcuts";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { getAvatarUrl } from "~/utils/helpers";
|
||||
|
||||
@@ -31,6 +32,7 @@ export default function UserMenu({
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { openModal } = useModal();
|
||||
const { openLegend } = useKeyboardShortcuts();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -169,6 +171,19 @@ export default function UserMenu({
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<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>
|
||||
<Link
|
||||
href="mailto:support@kan.bn"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Button, Menu, Transition } from "@headlessui/react";
|
||||
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 { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import CommandPallette from "./CommandPallette";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
export default function WorkspaceMenu({
|
||||
isCollapsed = false,
|
||||
@@ -18,17 +20,17 @@ export default function WorkspaceMenu({
|
||||
const { openModal } = useModal();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
|
||||
event.preventDefault();
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
const { tooltipContent: commandPaletteShortcutTooltipContent } =
|
||||
useKeyboardShortcut({
|
||||
type: "PRESS",
|
||||
stroke: {
|
||||
key: "k",
|
||||
modifiers: ["META"],
|
||||
},
|
||||
action: () => setIsOpen(true),
|
||||
description: t`Open command menu`,
|
||||
group: "GENERAL",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -84,15 +86,17 @@ export default function WorkspaceMenu({
|
||||
</span>
|
||||
)}
|
||||
</Menu.Button>
|
||||
<Button
|
||||
className={twMerge(
|
||||
"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)}
|
||||
>
|
||||
<HiMagnifyingGlass className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Tooltip content={commandPaletteShortcutTooltipContent}>
|
||||
<Button
|
||||
className={twMerge(
|
||||
"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)}
|
||||
>
|
||||
<HiMagnifyingGlass className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,8 @@ export const env = createEnv({
|
||||
VK_CLIENT_SECRET: z.string().optional(),
|
||||
LINKEDIN_CLIENT_ID: z.string().optional(),
|
||||
LINKEDIN_CLIENT_SECRET: z.string().optional(),
|
||||
NOVU_API_KEY: z.string().optional(),
|
||||
EMAIL_UNSUBSCRIBE_SECRET: z.string().optional(),
|
||||
// Generic OIDC Provider
|
||||
OIDC_CLIENT_ID: z.string().optional(),
|
||||
OIDC_CLIENT_SECRET: z.string().optional(),
|
||||
@@ -87,9 +89,10 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: 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_AVATAR_BUCKET_NAME: z.string().optional(),
|
||||
NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME: z.string().optional(),
|
||||
NEXT_PUBLIC_STORAGE_DOMAIN: z.string().optional(),
|
||||
NEXT_PUBLIC_ALLOW_CREDENTIALS: z
|
||||
.string()
|
||||
@@ -125,6 +128,8 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
|
||||
NEXT_PUBLIC_STORAGE_URL: process.env.NEXT_PUBLIC_STORAGE_URL,
|
||||
NEXT_PUBLIC_AVATAR_BUCKET_NAME: process.env.NEXT_PUBLIC_AVATAR_BUCKET_NAME,
|
||||
NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME:
|
||||
process.env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME,
|
||||
NEXT_PUBLIC_STORAGE_DOMAIN: process.env.NEXT_PUBLIC_STORAGE_DOMAIN,
|
||||
NEXT_PUBLIC_ALLOW_CREDENTIALS: process.env.NEXT_PUBLIC_ALLOW_CREDENTIALS,
|
||||
NEXT_PUBLIC_DISABLE_SIGN_UP: process.env.NEXT_PUBLIC_DISABLE_SIGN_UP,
|
||||
|
||||
@@ -31,17 +31,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} Labels"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} nicht gefunden"
|
||||
|
||||
@@ -79,8 +79,8 @@ msgstr "Konto"
|
||||
msgid "Account deleted"
|
||||
msgstr "Konto gelöscht"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Aktivität"
|
||||
|
||||
@@ -109,7 +109,7 @@ msgstr "Element hinzufügen..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Checkliste hinzufügen"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Beschreibung hinzufügen... (tippe '/' um Befehle zu öffnen oder '@' um jemanden zu erwähnen)"
|
||||
|
||||
@@ -270,6 +270,10 @@ msgstr "hat <0>{0}</0> der Karte zugewiesen"
|
||||
msgid "Assignees"
|
||||
msgstr "Zugewiesene Personen"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Anhang hochgeladen"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "Warten auf Kunden"
|
||||
@@ -304,9 +308,9 @@ msgstr "Abrechnungsportal"
|
||||
msgid "Blog Post"
|
||||
msgstr "Blogbeitrag"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Board"
|
||||
|
||||
@@ -323,7 +327,7 @@ msgstr "Der Name des Boards darf 100 Zeichen nicht überschreiten"
|
||||
msgid "Board name is required"
|
||||
msgstr "Boardname ist erforderlich"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Board nicht gefunden"
|
||||
|
||||
@@ -331,19 +335,19 @@ msgstr "Board nicht gefunden"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Board-Freigabe & Einladungen"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "Board-URL darf nur Buchstaben, Zahlen und Bindestriche enthalten"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "Board-URL darf 60 Zeichen nicht überschreiten"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "Board-URL in die Zwischenablage kopiert"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "Board-URL muss mindestens 3 Zeichen lang sein"
|
||||
|
||||
@@ -352,7 +356,7 @@ msgstr "Board-URL muss mindestens 3 Zeichen lang sein"
|
||||
msgid "Board visibility"
|
||||
msgstr "Board-Sichtbarkeit"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Board-Sichtbarkeit aktualisiert"
|
||||
|
||||
@@ -387,15 +391,16 @@ msgstr "Fehlerbericht"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Karte"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Karte nicht gefunden"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Kartentitel"
|
||||
|
||||
@@ -428,7 +433,7 @@ msgstr "Checklistenname"
|
||||
msgid "Checklists"
|
||||
msgstr "Checklisten"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Filter löschen"
|
||||
|
||||
@@ -489,6 +494,10 @@ msgstr "hat ein Checklistenelement abgeschlossen"
|
||||
msgid "completed checklist item <0>{0}</0>"
|
||||
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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Bestätigen Sie Ihr neues Passwort"
|
||||
@@ -519,12 +528,12 @@ msgstr "Kontaktiere uns"
|
||||
msgid "Content Creation"
|
||||
msgstr "Content-Erstellung"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Fortfahren mit "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Fortfahren mit {0}"
|
||||
|
||||
@@ -542,8 +551,8 @@ msgid "Create {0}"
|
||||
msgstr "{0} erstellen"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Weitere erstellen"
|
||||
|
||||
@@ -551,7 +560,7 @@ msgstr "Weitere erstellen"
|
||||
msgid "Create API key"
|
||||
msgstr "API-Schlüssel erstellen"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Karte erstellen"
|
||||
|
||||
@@ -567,7 +576,7 @@ msgstr "Einladungslink erstellen"
|
||||
msgid "Create label"
|
||||
msgstr "Label erstellen"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Liste erstellen"
|
||||
|
||||
@@ -580,12 +589,12 @@ msgstr "Neue(n) {0} erstellen"
|
||||
msgid "Create new key"
|
||||
msgstr "Neuen Schlüssel erstellen"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Neues Label erstellen"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Neue Liste erstellen"
|
||||
|
||||
@@ -743,6 +752,10 @@ msgstr "Anzeigename aktualisiert"
|
||||
msgid "Do you offer a free plan?"
|
||||
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
|
||||
msgid "docs"
|
||||
msgstr "Dokumente"
|
||||
@@ -769,8 +782,12 @@ msgstr "Du hast noch kein Konto? <0><1>Registrieren</1></0>"
|
||||
msgid "Done"
|
||||
msgstr "Erledigt"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Download fehlgeschlagen"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Board-URL bearbeiten"
|
||||
|
||||
@@ -782,7 +799,7 @@ msgstr "Kommentar bearbeiten"
|
||||
msgid "Edit label"
|
||||
msgstr "Label bearbeiten"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Workspace-URL bearbeiten"
|
||||
|
||||
@@ -790,7 +807,7 @@ msgstr "Workspace-URL bearbeiten"
|
||||
msgid "Editing"
|
||||
msgstr "Bearbeitung"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "E-Mail"
|
||||
|
||||
@@ -810,11 +827,11 @@ msgstr "Geben Sie Ihr aktuelles Passwort ein"
|
||||
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."
|
||||
|
||||
#: src/components/AuthForm.tsx:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Gib deine E-Mail-Adresse ein"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Gib deinen Namen ein"
|
||||
|
||||
@@ -822,7 +839,7 @@ msgstr "Gib deinen Namen ein"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Geben Sie Ihr neues Passwort ein"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Gib dein Passwort ein"
|
||||
|
||||
@@ -931,6 +948,11 @@ msgstr "Board konnte nicht erstellt werden"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Anmeldung mit {0} fehlgeschlagen. Bitte versuche es erneut."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Fehler beim Hochladen des Anhangs. Bitte versuchen Sie es erneut."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -974,7 +996,7 @@ msgstr "Feedback"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Feedback gesendet"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
|
||||
@@ -1028,7 +1050,7 @@ msgstr "Loslegen"
|
||||
msgid "Get started by creating a new {0}"
|
||||
msgstr "Beginnen Sie, indem Sie eine(n) neue(n) {0} erstellen"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Beginne mit dem Erstellen einer neuen Liste"
|
||||
|
||||
@@ -1242,9 +1264,9 @@ msgstr "Kanban neu gedacht"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Beachten Sie, dass diese Aktion nicht rückgängig gemacht werden kann."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Labels"
|
||||
|
||||
@@ -1294,18 +1316,22 @@ msgstr "Lizenz"
|
||||
msgid "Light"
|
||||
msgstr "Hell"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Link kopiert"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "Liste"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "Listenname"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Listen"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Login | kan.bn"
|
||||
@@ -1326,7 +1352,7 @@ msgstr "Von Teams weltweit geliebt"
|
||||
msgid "Low Priority"
|
||||
msgstr "Niedrige Priorität"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "Magic Link"
|
||||
|
||||
@@ -1348,9 +1374,9 @@ msgid "Medium Priority"
|
||||
msgstr "Mittlere Priorität"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Mitglieder"
|
||||
@@ -1403,7 +1429,7 @@ msgstr "Neue(r) {0}"
|
||||
msgid "New API key"
|
||||
msgstr "Neuer API-Schlüssel"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "Neue Karte"
|
||||
|
||||
@@ -1419,8 +1445,8 @@ msgstr "Neuer Import"
|
||||
msgid "New label"
|
||||
msgstr "Neues Label"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "Neue Liste"
|
||||
|
||||
@@ -1457,6 +1483,10 @@ msgstr "Nächste Schritte"
|
||||
msgid "No {0}"
|
||||
msgstr "Keine {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "Derzeit sind keine Authentifizierungsmethoden verfügbar"
|
||||
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:242
|
||||
msgid "No boards found"
|
||||
msgstr "Keine Boards gefunden"
|
||||
@@ -1465,7 +1495,11 @@ msgstr "Keine Boards gefunden"
|
||||
msgid "No credit card required"
|
||||
msgstr "Keine Kreditkarte erforderlich"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "No download URL available for this attachment."
|
||||
msgstr "Für diesen Anhang ist keine Download-URL verfügbar."
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
msgid "No lists"
|
||||
msgstr "Keine Listen"
|
||||
|
||||
@@ -1497,7 +1531,7 @@ msgstr "Open Source"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Open-Source-Freunde"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "oder"
|
||||
|
||||
@@ -1579,15 +1613,15 @@ msgstr "Plattform"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Bitte bestätigen Sie Ihr neues Passwort"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Bitte gib eine gültige E-Mail-Adresse ein"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Bitte gib einen gültigen Namen ein"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Bitte gib ein gültiges Passwort ein"
|
||||
|
||||
@@ -1598,15 +1632,16 @@ msgstr "Bitte wähle eine Datei zum Hochladen aus."
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1620,7 +1655,7 @@ msgstr "Bitte wähle eine Datei zum Hochladen aus."
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1664,7 +1699,7 @@ msgstr "Prioritäts-E-Mail-Support"
|
||||
msgid "Privacy policy"
|
||||
msgstr "Datenschutzrichtlinie"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Privat"
|
||||
|
||||
@@ -1684,7 +1719,7 @@ msgstr "Profilbild aktualisiert"
|
||||
msgid "Profile picture"
|
||||
msgstr "Profilbild"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Öffentlich"
|
||||
|
||||
@@ -1886,7 +1921,7 @@ msgstr "Registrierung deaktiviert"
|
||||
msgid "Sign up is currently disabled. Please try again later."
|
||||
msgstr "Die Registrierung ist derzeit deaktiviert. Bitte versuche es später erneut."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "Registrieren mit "
|
||||
|
||||
@@ -1953,8 +1988,8 @@ msgstr "Team-Plan"
|
||||
msgid "Teams"
|
||||
msgstr "Teams"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
@@ -2004,7 +2039,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "Die open source <0/> alternative zu Trello"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "Die Sichtbarkeit deines Boards wurde auf {0} gesetzt."
|
||||
|
||||
@@ -2028,11 +2063,11 @@ msgstr "Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "Dieser API-Schlüssel wird nur einmal angezeigt. Bitte speichern Sie ihn an einem sicheren Ort."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "Dieses Board ist privat oder existiert nicht"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "Diese Board-URL ist bereits vergeben"
|
||||
|
||||
@@ -2102,7 +2137,7 @@ msgstr "Checklistenelement kann nicht hinzugefügt werden"
|
||||
msgid "Unable to add comment"
|
||||
msgstr "Kommentar konnte nicht hinzugefügt werden"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "Karte konnte nicht erstellt werden"
|
||||
|
||||
@@ -2110,7 +2145,7 @@ msgstr "Karte konnte nicht erstellt werden"
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "Checkliste kann nicht erstellt werden"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "Liste konnte nicht erstellt werden"
|
||||
|
||||
@@ -2123,6 +2158,10 @@ msgstr "Vorlage konnte nicht erstellt werden"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "Workspace konnte nicht erstellt werden"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "Anhang kann nicht gelöscht werden"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "Karte konnte nicht gelöscht werden"
|
||||
@@ -2147,16 +2186,16 @@ msgstr "Mitglied konnte nicht entfernt werden"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "Feedback konnte nicht gesendet werden"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "Board-URL kann nicht aktualisiert werden"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "Board-Sichtbarkeit konnte nicht aktualisiert werden"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "Karte konnte nicht aktualisiert werden"
|
||||
|
||||
@@ -2176,7 +2215,7 @@ msgstr "Kommentar konnte nicht aktualisiert werden"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "Labels konnten nicht aktualisiert werden"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "Liste konnte nicht aktualisiert werden"
|
||||
@@ -2232,7 +2271,12 @@ msgstr "Unbegrenzte Mitglieder"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Unbegrenzte Workspaces"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: 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/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2271,6 +2315,11 @@ msgstr "Upgrade auf Pro"
|
||||
msgid "Upgrade to Pro ($29/month)"
|
||||
msgstr "Upgrade auf Pro ($29/Monat)"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Hochladen fehlgeschlagen"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2320,7 +2369,7 @@ msgstr "Verwalte deine Abrechnung und dein Abonnement."
|
||||
msgid "View docs"
|
||||
msgstr "Dokumentation ansehen"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "Nur ansehen"
|
||||
|
||||
@@ -2328,11 +2377,11 @@ msgstr "Nur ansehen"
|
||||
msgid "View our roadmap."
|
||||
msgstr "Sieh dir unsere roadmap an."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "Workspace anzeigen"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Sichtbarkeit"
|
||||
|
||||
@@ -2340,6 +2389,10 @@ msgstr "Sichtbarkeit"
|
||||
msgid "We are using the <0>AGPL-3.0 license</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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "Wir stehen erst am anfang. "
|
||||
@@ -2369,10 +2422,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "Warum ein open source Trello entwickeln?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Arbeitsbereich"
|
||||
@@ -2478,6 +2531,10 @@ msgstr "Sie haben sich erfolgreich angemeldet."
|
||||
msgid "You have been signed up successfully."
|
||||
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
|
||||
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!"
|
||||
@@ -2502,6 +2559,10 @@ msgstr "Deine Boards wurden importiert."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Dein Anzeigename wurde aktualisiert."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Ihre Datei wurde erfolgreich hochgeladen."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "Ihr Passwort wurde geändert."
|
||||
@@ -2518,6 +2579,10 @@ msgstr "Dein Trello-Konto wurde getrennt."
|
||||
msgid "Your Trello account is connected."
|
||||
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
|
||||
msgid "Your workspace description has been updated."
|
||||
msgstr "Deine Workspace-Beschreibung wurde aktualisiert."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,17 +31,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} labels"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} not found"
|
||||
|
||||
@@ -91,8 +91,12 @@ msgstr "Account"
|
||||
msgid "Account deleted"
|
||||
msgstr "Account deleted"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/providers/keyboard-shortcuts.tsx:55
|
||||
#~ msgid "Actions"
|
||||
#~ msgstr "Actions"
|
||||
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Activity"
|
||||
|
||||
@@ -121,7 +125,7 @@ msgstr "Add an item..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Add checklist"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Add description... (type '/' to open commands or '@' to mention)"
|
||||
|
||||
@@ -306,6 +310,10 @@ msgstr "assigned <0>{0}</0> to the card"
|
||||
msgid "Assignees"
|
||||
msgstr "Assignees"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Attachment uploaded"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "Awaiting Customer"
|
||||
@@ -344,9 +352,9 @@ msgstr "Billing portal"
|
||||
msgid "Blog Post"
|
||||
msgstr "Blog Post"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Board"
|
||||
|
||||
@@ -372,7 +380,7 @@ msgstr "Board name cannot exceed 100 characters"
|
||||
msgid "Board name is required"
|
||||
msgstr "Board name is required"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Board not found"
|
||||
|
||||
@@ -380,19 +388,19 @@ msgstr "Board not found"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Board sharing & invites"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "Board URL can only contain letters, numbers, and hyphens"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "Board URL cannot exceed 60 characters"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "Board URL copied to clipboard"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "Board URL must be at least 3 characters long"
|
||||
|
||||
@@ -401,7 +409,7 @@ msgstr "Board URL must be at least 3 characters long"
|
||||
msgid "Board visibility"
|
||||
msgstr "Board visibility"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Board visibility updated"
|
||||
|
||||
@@ -444,15 +452,16 @@ msgstr "Bug Report"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Card"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Card not found"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Card title"
|
||||
|
||||
@@ -489,7 +498,7 @@ msgstr "Checklist name"
|
||||
msgid "Checklists"
|
||||
msgstr "Checklists"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Clear filters"
|
||||
|
||||
@@ -554,6 +563,10 @@ msgstr "completed checklist item <0>{0}</0>"
|
||||
#~ msgid "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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Confirm your new password"
|
||||
@@ -584,12 +597,12 @@ msgstr "Contact us"
|
||||
msgid "Content Creation"
|
||||
msgstr "Content Creation"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Continue with "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Continue with {0}"
|
||||
|
||||
@@ -607,8 +620,8 @@ msgid "Create {0}"
|
||||
msgstr "Create {0}"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Create another"
|
||||
|
||||
@@ -624,7 +637,7 @@ msgstr "Create API key"
|
||||
#~ msgid "Create board"
|
||||
#~ msgstr "Create board"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Create card"
|
||||
|
||||
@@ -640,7 +653,7 @@ msgstr "Create invite link"
|
||||
msgid "Create label"
|
||||
msgstr "Create label"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Create list"
|
||||
|
||||
@@ -657,12 +670,12 @@ msgstr "Create new {0}"
|
||||
msgid "Create new key"
|
||||
msgstr "Create new key"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Create new label"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Create new list"
|
||||
|
||||
@@ -832,6 +845,10 @@ msgstr "Display name updated"
|
||||
msgid "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
|
||||
msgid "docs"
|
||||
msgstr "docs"
|
||||
@@ -858,8 +875,12 @@ msgstr "Don't have an account? <0><1>Sign up</1></0>"
|
||||
msgid "Done"
|
||||
msgstr "Done"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Download failed"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Edit board URL"
|
||||
|
||||
@@ -871,7 +892,7 @@ msgstr "Edit comment"
|
||||
msgid "Edit label"
|
||||
msgstr "Edit label"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Edit workspace URL"
|
||||
|
||||
@@ -879,7 +900,7 @@ msgstr "Edit workspace URL"
|
||||
msgid "Editing"
|
||||
msgstr "Editing"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "email"
|
||||
|
||||
@@ -899,11 +920,11 @@ msgstr "Enter your current password"
|
||||
msgid "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:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Enter your email address"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Enter your name"
|
||||
|
||||
@@ -911,7 +932,7 @@ msgstr "Enter your name"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Enter your new password"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Enter your password"
|
||||
|
||||
@@ -1020,6 +1041,11 @@ msgstr "Failed to create board"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Failed to login with {0}. Please try again."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Failed to upload attachment. Please try again."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -1063,7 +1089,7 @@ msgstr "Feedback"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Feedback sent"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
|
||||
@@ -1108,6 +1134,10 @@ msgstr "Full-time"
|
||||
msgid "Fun"
|
||||
msgstr "Fun"
|
||||
|
||||
#: src/providers/keyboard-shortcuts.tsx:53
|
||||
#~ msgid "General"
|
||||
#~ msgstr "General"
|
||||
|
||||
#: src/views/auth/signup/index.tsx:69
|
||||
#: src/views/home/components/Cta.tsx:61
|
||||
#: src/views/home/components/Header.tsx:106
|
||||
@@ -1131,7 +1161,7 @@ msgstr "Get started by creating a new {0}"
|
||||
#~ msgid "Get started by creating a new board"
|
||||
#~ msgstr "Get started by creating a new board"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Get started by creating a new list"
|
||||
|
||||
@@ -1165,6 +1195,22 @@ msgstr "Go Home"
|
||||
msgid "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
|
||||
msgid "High Priority"
|
||||
msgstr "High Priority"
|
||||
@@ -1353,9 +1399,13 @@ msgstr "Kanban reimagined"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Keep in mind that this action is irreversible."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/providers/keyboard-shortcuts.tsx:320
|
||||
#~ msgid "Keyboard Shortcuts"
|
||||
#~ msgstr "Keyboard Shortcuts"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Labels"
|
||||
|
||||
@@ -1405,18 +1455,22 @@ msgstr "License"
|
||||
msgid "Light"
|
||||
msgstr "Light"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Link copied"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "List"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "List name"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Lists"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Login | kan.bn"
|
||||
@@ -1437,7 +1491,7 @@ msgstr "Loved by teams worldwide"
|
||||
msgid "Low Priority"
|
||||
msgstr "Low Priority"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "magic link"
|
||||
|
||||
@@ -1459,9 +1513,9 @@ msgid "Medium Priority"
|
||||
msgstr "Medium Priority"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Members"
|
||||
@@ -1504,6 +1558,10 @@ msgstr "moved the card to another list"
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/providers/keyboard-shortcuts.tsx:54
|
||||
#~ msgid "Navigation"
|
||||
#~ msgstr "Navigation"
|
||||
|
||||
#: src/components/FeedbackModal.tsx:95
|
||||
msgid "Need help?"
|
||||
msgstr "Need help?"
|
||||
@@ -1526,7 +1584,7 @@ msgstr "New API key"
|
||||
#~ msgid "New board"
|
||||
#~ msgstr "New board"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "New card"
|
||||
|
||||
@@ -1542,8 +1600,8 @@ msgstr "New import"
|
||||
msgid "New label"
|
||||
msgstr "New label"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "New list"
|
||||
|
||||
@@ -1584,6 +1642,10 @@ msgstr "Next Steps"
|
||||
msgid "No {0}"
|
||||
msgstr "No {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "No authentication methods are currently available"
|
||||
|
||||
#: src/views/boards/components/BoardsList.tsx:35
|
||||
#~ msgid "No boards"
|
||||
#~ msgstr "No boards"
|
||||
@@ -1596,7 +1658,15 @@ msgstr "No boards found"
|
||||
msgid "No credit card required"
|
||||
msgstr "No credit card required"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "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
|
||||
msgid "No lists"
|
||||
msgstr "No lists"
|
||||
|
||||
@@ -1620,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."
|
||||
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
|
||||
msgid "Open source"
|
||||
msgstr "Open source"
|
||||
@@ -1628,7 +1706,7 @@ msgstr "Open source"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Open Source Friends"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "or"
|
||||
|
||||
@@ -1710,15 +1788,15 @@ msgstr "Platform"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Please confirm your new password"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Please enter a valid email address"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Please enter a valid name"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Please enter a valid password"
|
||||
|
||||
@@ -1729,15 +1807,16 @@ msgstr "Please select a file to upload."
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1751,7 +1830,7 @@ msgstr "Please select a file to upload."
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1795,7 +1874,7 @@ msgstr "Priority email support"
|
||||
msgid "Privacy policy"
|
||||
msgstr "Privacy policy"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Private"
|
||||
|
||||
@@ -1815,7 +1894,7 @@ msgstr "Profile image updated"
|
||||
msgid "Profile picture"
|
||||
msgstr "Profile picture"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Public"
|
||||
|
||||
@@ -2012,6 +2091,10 @@ msgstr "Settings | Workspace"
|
||||
#~ msgid "Share invite link"
|
||||
#~ msgstr "Share invite link"
|
||||
|
||||
#: src/components/UserMenu.tsx:184
|
||||
#~ msgid "Shortcuts"
|
||||
#~ msgstr "Shortcuts"
|
||||
|
||||
#: src/views/home/components/Header.tsx:104
|
||||
msgid "Sign in"
|
||||
msgstr "Sign in"
|
||||
@@ -2037,7 +2120,7 @@ msgstr "Sign up disabled"
|
||||
msgid "Sign up is currently disabled. Please try again later."
|
||||
msgstr "Sign up is currently disabled. Please try again later."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "Sign up with "
|
||||
|
||||
@@ -2104,8 +2187,8 @@ msgstr "Team Plan"
|
||||
msgid "Teams"
|
||||
msgstr "Teams"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Template"
|
||||
|
||||
@@ -2155,7 +2238,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "The open source <0/> alternative to Trello"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "The visibility of your board has been set to {0}."
|
||||
|
||||
@@ -2179,11 +2262,11 @@ msgstr "This action can't be undone."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "This API key will only be shown once. Please save it in a secure location."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "This board is private or does not exist"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "This board URL has already been taken"
|
||||
|
||||
@@ -2261,7 +2344,7 @@ msgstr "Unable to add checklist item"
|
||||
msgid "Unable to add comment"
|
||||
msgstr "Unable to add comment"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "Unable to create card"
|
||||
|
||||
@@ -2269,7 +2352,7 @@ msgstr "Unable to create card"
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "Unable to create checklist"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "Unable to create list"
|
||||
|
||||
@@ -2282,6 +2365,10 @@ msgstr "Unable to create template"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "Unable to create workspace"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "Unable to delete attachment"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "Unable to delete card"
|
||||
@@ -2306,16 +2393,16 @@ msgstr "Unable to remove member"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "Unable to send feedback"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "Unable to update board URL"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "Unable to update board visibility"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "Unable to update card"
|
||||
|
||||
@@ -2335,7 +2422,7 @@ msgstr "Unable to update comment"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "Unable to update labels"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "Unable to update list"
|
||||
@@ -2395,7 +2482,12 @@ msgstr "Unlimited members"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Unlimited workspaces"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: 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/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2446,6 +2538,11 @@ msgstr "Upgrade to Pro ($29/month)"
|
||||
#~ msgid "Upgrade to Team Plan"
|
||||
#~ msgstr "Upgrade to Team Plan"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Upload failed"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2495,7 +2592,7 @@ msgstr "View and manage your billing and subscription."
|
||||
msgid "View docs"
|
||||
msgstr "View docs"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "View only"
|
||||
|
||||
@@ -2503,11 +2600,11 @@ msgstr "View only"
|
||||
msgid "View our roadmap."
|
||||
msgstr "View our roadmap."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "View workspace"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Visibility"
|
||||
|
||||
@@ -2515,6 +2612,10 @@ msgstr "Visibility"
|
||||
msgid "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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "We're just getting started. "
|
||||
@@ -2552,10 +2653,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "Why make an open source Trello?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Workspace"
|
||||
@@ -2665,6 +2766,10 @@ msgstr "You have been logged in successfully."
|
||||
msgid "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
|
||||
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!"
|
||||
@@ -2689,6 +2794,10 @@ msgstr "Your boards have been imported."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Your display name has been updated."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Your file has been uploaded successfully."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "Your password has been changed."
|
||||
@@ -2705,6 +2814,10 @@ msgstr "Your Trello account has been disconnected."
|
||||
msgid "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
|
||||
msgid "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,17 +31,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} etiquetas"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} no encontrado"
|
||||
|
||||
@@ -79,8 +79,8 @@ msgstr "Cuenta"
|
||||
msgid "Account deleted"
|
||||
msgstr "Cuenta eliminada"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Actividad"
|
||||
|
||||
@@ -109,7 +109,7 @@ msgstr "Añadir un elemento..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Añadir lista de verificación"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Añadir descripción... (escribe '/' para abrir comandos o '@' para mencionar)"
|
||||
|
||||
@@ -270,6 +270,10 @@ msgstr "asignó <0>{0}</0> a la tarjeta"
|
||||
msgid "Assignees"
|
||||
msgstr "Asignados"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Archivo adjunto subido"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "Esperando al cliente"
|
||||
@@ -304,9 +308,9 @@ msgstr "Portal de facturación"
|
||||
msgid "Blog Post"
|
||||
msgstr "Entrada de blog"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Tablero"
|
||||
|
||||
@@ -323,7 +327,7 @@ msgstr "El nombre del tablero no puede exceder los 100 caracteres"
|
||||
msgid "Board name is required"
|
||||
msgstr "El nombre del tablero es obligatorio"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Tablero no encontrado"
|
||||
|
||||
@@ -331,19 +335,19 @@ msgstr "Tablero no encontrado"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Compartir tablero e invitaciones"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "La URL del tablero solo puede contener letras, números y guiones"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "La URL del tablero no puede exceder los 60 caracteres"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "URL del tablero copiada al portapapeles"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "La URL del tablero debe tener al menos 3 caracteres"
|
||||
|
||||
@@ -352,7 +356,7 @@ msgstr "La URL del tablero debe tener al menos 3 caracteres"
|
||||
msgid "Board visibility"
|
||||
msgstr "Visibilidad del tablero"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Visibilidad del tablero actualizada"
|
||||
|
||||
@@ -387,15 +391,16 @@ msgstr "Informe de error"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Tarjeta"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Tarjeta no encontrada"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Título de la tarjeta"
|
||||
|
||||
@@ -428,7 +433,7 @@ msgstr "Nombre de la lista de verificación"
|
||||
msgid "Checklists"
|
||||
msgstr "Listas de verificación"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Borrar filtros"
|
||||
|
||||
@@ -489,6 +494,10 @@ msgstr "completó un elemento de la lista de verificación"
|
||||
msgid "completed checklist item <0>{0}</0>"
|
||||
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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Confirma tu nueva contraseña"
|
||||
@@ -519,12 +528,12 @@ msgstr "Contáctanos"
|
||||
msgid "Content Creation"
|
||||
msgstr "Creación de contenido"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Continuar con "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Continuar con {0}"
|
||||
|
||||
@@ -542,8 +551,8 @@ msgid "Create {0}"
|
||||
msgstr "Crear {0}"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Crear otro"
|
||||
|
||||
@@ -551,7 +560,7 @@ msgstr "Crear otro"
|
||||
msgid "Create API key"
|
||||
msgstr "Crear clave API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Crear tarjeta"
|
||||
|
||||
@@ -567,7 +576,7 @@ msgstr "Crear enlace de invitación"
|
||||
msgid "Create label"
|
||||
msgstr "Crear etiqueta"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Crear lista"
|
||||
|
||||
@@ -580,12 +589,12 @@ msgstr "Crear nuevo {0}"
|
||||
msgid "Create new key"
|
||||
msgstr "Crear nueva clave"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Crear nueva etiqueta"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Crear nueva lista"
|
||||
|
||||
@@ -743,6 +752,10 @@ msgstr "Nombre visible actualizado"
|
||||
msgid "Do you offer a free plan?"
|
||||
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
|
||||
msgid "docs"
|
||||
msgstr "documentos"
|
||||
@@ -769,8 +782,12 @@ msgstr "¿No tienes una cuenta? <0><1>Regístrate</1></0>"
|
||||
msgid "Done"
|
||||
msgstr "Terminado"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Error en la descarga"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Editar URL del tablero"
|
||||
|
||||
@@ -782,7 +799,7 @@ msgstr "Editar comentario"
|
||||
msgid "Edit label"
|
||||
msgstr "Editar etiqueta"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Editar URL del espacio de trabajo"
|
||||
|
||||
@@ -790,7 +807,7 @@ msgstr "Editar URL del espacio de trabajo"
|
||||
msgid "Editing"
|
||||
msgstr "Editando"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "correo electrónico"
|
||||
|
||||
@@ -810,11 +827,11 @@ msgstr "Introduce tu contraseña actual"
|
||||
msgid "Enter your current password and choose a new secure password."
|
||||
msgstr "Introduce tu contraseña actual y elige una nueva contraseña segura."
|
||||
|
||||
#: src/components/AuthForm.tsx:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Introduce tu dirección de correo electrónico"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Introduce tu nombre"
|
||||
|
||||
@@ -822,7 +839,7 @@ msgstr "Introduce tu nombre"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Introduce tu nueva contraseña"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Introduce tu contraseña"
|
||||
|
||||
@@ -931,6 +948,11 @@ msgstr "Error al crear el tablero"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Error al iniciar sesión con {0}. Por favor, inténtalo de nuevo."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Error al subir el archivo adjunto. Por favor, inténtalo de nuevo."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -974,7 +996,7 @@ msgstr "Comentarios"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Comentarios enviados"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Filtrar"
|
||||
|
||||
@@ -1028,7 +1050,7 @@ msgstr "Comenzar"
|
||||
msgid "Get started by creating a new {0}"
|
||||
msgstr "Comienza creando un nuevo {0}"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Comienza creando una nueva lista"
|
||||
|
||||
@@ -1242,9 +1264,9 @@ msgstr "Kanban reinventado"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Ten en cuenta que esta acción es irreversible."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
@@ -1294,18 +1316,22 @@ msgstr "Licencia"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Enlace copiado"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "Lista"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "Nombre de la lista"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Listas"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Iniciar sesión | kan.bn"
|
||||
@@ -1326,7 +1352,7 @@ msgstr "Amado por equipos en todo el mundo"
|
||||
msgid "Low Priority"
|
||||
msgstr "Prioridad baja"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "enlace mágico"
|
||||
|
||||
@@ -1348,9 +1374,9 @@ msgid "Medium Priority"
|
||||
msgstr "Prioridad media"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Miembros"
|
||||
@@ -1403,7 +1429,7 @@ msgstr "Nuevo {0}"
|
||||
msgid "New API key"
|
||||
msgstr "Nueva clave API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "Nueva tarjeta"
|
||||
|
||||
@@ -1419,8 +1445,8 @@ msgstr "Nueva importación"
|
||||
msgid "New label"
|
||||
msgstr "Nueva etiqueta"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "Nueva lista"
|
||||
|
||||
@@ -1457,6 +1483,10 @@ msgstr "Próximos pasos"
|
||||
msgid "No {0}"
|
||||
msgstr "Sin {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "No hay métodos de autenticación disponibles actualmente"
|
||||
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:242
|
||||
msgid "No boards found"
|
||||
msgstr "No se encontraron tableros"
|
||||
@@ -1465,7 +1495,11 @@ msgstr "No se encontraron tableros"
|
||||
msgid "No credit card required"
|
||||
msgstr "No se requiere tarjeta de crédito"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "No download URL available for this attachment."
|
||||
msgstr "No hay URL de descarga disponible para este archivo adjunto."
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
msgid "No lists"
|
||||
msgstr "Sin listas"
|
||||
|
||||
@@ -1497,7 +1531,7 @@ msgstr "Código abierto"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Amigos de código abierto"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "o"
|
||||
|
||||
@@ -1579,15 +1613,15 @@ msgstr "Plataforma"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Por favor, confirma tu nueva contraseña"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Por favor, introduce una dirección de correo electrónico válida"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Por favor, introduce un nombre válido"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Por favor, introduce una contraseña válida"
|
||||
|
||||
@@ -1598,15 +1632,16 @@ msgstr "Por favor selecciona un archivo para subir."
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1620,7 +1655,7 @@ msgstr "Por favor selecciona un archivo para subir."
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1664,7 +1699,7 @@ msgstr "Soporte prioritario por email"
|
||||
msgid "Privacy policy"
|
||||
msgstr "Política de privacidad"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Privado"
|
||||
|
||||
@@ -1684,7 +1719,7 @@ msgstr "Imagen de perfil actualizada"
|
||||
msgid "Profile picture"
|
||||
msgstr "Foto de perfil"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Público"
|
||||
|
||||
@@ -1886,7 +1921,7 @@ msgstr "Registro deshabilitado"
|
||||
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."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "Registrarse con "
|
||||
|
||||
@@ -1953,8 +1988,8 @@ msgstr "Plan de Equipo"
|
||||
msgid "Teams"
|
||||
msgstr "Equipos"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
@@ -2004,7 +2039,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "La alternativa de código abierto <0/> a Trello"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "La visibilidad de tu tablero ha sido establecida a {0}."
|
||||
|
||||
@@ -2028,11 +2063,11 @@ msgstr "Esta acción no se puede deshacer."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "Esta clave API solo se mostrará una vez. Por favor, guárdala en un lugar seguro."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "Este tablero es privado o no existe"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "Esta URL de tablero ya ha sido utilizada"
|
||||
|
||||
@@ -2102,7 +2137,7 @@ msgstr "No se puede añadir el elemento a la lista de verificación"
|
||||
msgid "Unable to add comment"
|
||||
msgstr "No se puede añadir el comentario"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "No se puede crear la tarjeta"
|
||||
|
||||
@@ -2110,7 +2145,7 @@ msgstr "No se puede crear la tarjeta"
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "No se puede crear la lista de verificación"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "No se puede crear la lista"
|
||||
|
||||
@@ -2123,6 +2158,10 @@ msgstr "No se puede crear la plantilla"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "No se puede crear el espacio de trabajo"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "No se puede eliminar el archivo adjunto"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "No se puede eliminar la tarjeta"
|
||||
@@ -2147,16 +2186,16 @@ msgstr "No se puede eliminar al miembro"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "No se pudieron enviar los comentarios"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "No se puede actualizar la URL del tablero"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "No se puede actualizar la visibilidad del tablero"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "No se puede actualizar la tarjeta"
|
||||
|
||||
@@ -2176,7 +2215,7 @@ msgstr "No se puede actualizar el comentario"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "No se pueden actualizar las etiquetas"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "No se puede actualizar la lista"
|
||||
@@ -2232,7 +2271,12 @@ msgstr "Miembros ilimitados"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Espacios de trabajo ilimitados"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: 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/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2271,6 +2315,11 @@ msgstr "Actualizar a Pro"
|
||||
msgid "Upgrade to Pro ($29/month)"
|
||||
msgstr "Actualizar a Pro ($29/mes)"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Error al subir"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2320,7 +2369,7 @@ msgstr "Ver y gestionar tu facturación y suscripción."
|
||||
msgid "View docs"
|
||||
msgstr "Ver documentación"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "Solo visualización"
|
||||
|
||||
@@ -2328,11 +2377,11 @@ msgstr "Solo visualización"
|
||||
msgid "View our roadmap."
|
||||
msgstr "Ver nuestra hoja de ruta."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "Ver espacio de trabajo"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Visibilidad"
|
||||
|
||||
@@ -2340,6 +2389,10 @@ msgstr "Visibilidad"
|
||||
msgid "We are using the <0>AGPL-3.0 license</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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "Apenas estamos comenzando. "
|
||||
@@ -2369,10 +2422,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "¿Por qué crear un Trello de código abierto?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Espacio de trabajo"
|
||||
@@ -2478,6 +2531,10 @@ msgstr "Has iniciado sesión correctamente."
|
||||
msgid "You have been signed up successfully."
|
||||
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
|
||||
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!"
|
||||
@@ -2502,6 +2559,10 @@ msgstr "Tus tableros han sido importados."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Tu nombre de visualización ha sido actualizado."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Tu archivo se ha subido correctamente."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "Tu contraseña ha sido cambiada."
|
||||
@@ -2518,6 +2579,10 @@ msgstr "Tu cuenta de Trello ha sido desconectada."
|
||||
msgid "Your Trello account is connected."
|
||||
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
|
||||
msgid "Your workspace description has been updated."
|
||||
msgstr "La descripción de tu espacio de trabajo ha sido actualizada."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,17 +31,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} étiquettes"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} introuvable"
|
||||
|
||||
@@ -79,8 +79,8 @@ msgstr "Compte"
|
||||
msgid "Account deleted"
|
||||
msgstr "Compte supprimé"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Activité"
|
||||
|
||||
@@ -109,7 +109,7 @@ msgstr "Ajouter un élément..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Ajouter une checklist"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Ajouter une description... (tapez '/' pour ouvrir les commandes ou '@' pour mentionner)"
|
||||
|
||||
@@ -270,6 +270,10 @@ msgstr "a assigné <0>{0}</0> à la carte"
|
||||
msgid "Assignees"
|
||||
msgstr "Assignés"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Pièce jointe téléversée"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "En attente du client"
|
||||
@@ -304,9 +308,9 @@ msgstr "Portail de facturation"
|
||||
msgid "Blog Post"
|
||||
msgstr "Article de blog"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Tableau"
|
||||
|
||||
@@ -323,7 +327,7 @@ msgstr "Le nom du tableau ne peut pas dépasser 100 caractères"
|
||||
msgid "Board name is required"
|
||||
msgstr "Le nom du tableau est requis"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Tableau introuvable"
|
||||
|
||||
@@ -331,19 +335,19 @@ msgstr "Tableau introuvable"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Partage de tableau & invitations"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "L'URL du tableau ne peut contenir que des lettres, des chiffres et des traits d'union"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "L'URL du tableau ne peut pas dépasser 60 caractères"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "URL du tableau copiée dans le presse-papiers"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "L'URL du tableau doit comporter au moins 3 caractères"
|
||||
|
||||
@@ -352,7 +356,7 @@ msgstr "L'URL du tableau doit comporter au moins 3 caractères"
|
||||
msgid "Board visibility"
|
||||
msgstr "Visibilité du tableau"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Visibilité du tableau mise à jour"
|
||||
|
||||
@@ -387,15 +391,16 @@ msgstr "Rapport de bug"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Carte"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Carte introuvable"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Titre de la carte"
|
||||
|
||||
@@ -428,7 +433,7 @@ msgstr "Nom de la checklist"
|
||||
msgid "Checklists"
|
||||
msgstr "Listes de contrôle"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Effacer les filtres"
|
||||
|
||||
@@ -489,6 +494,10 @@ msgstr "a terminé un élément de la checklist"
|
||||
msgid "completed checklist item <0>{0}</0>"
|
||||
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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Confirmez votre nouveau mot de passe"
|
||||
@@ -519,12 +528,12 @@ msgstr "Contactez-nous"
|
||||
msgid "Content Creation"
|
||||
msgstr "Création de contenu"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Continuer avec "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Continuer avec {0}"
|
||||
|
||||
@@ -542,8 +551,8 @@ msgid "Create {0}"
|
||||
msgstr "Créer {0}"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Créer un autre"
|
||||
|
||||
@@ -551,7 +560,7 @@ msgstr "Créer un autre"
|
||||
msgid "Create API key"
|
||||
msgstr "Créer une clé API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Créer une carte"
|
||||
|
||||
@@ -567,7 +576,7 @@ msgstr "Créer un lien d'invitation"
|
||||
msgid "Create label"
|
||||
msgstr "Créer une étiquette"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Créer une liste"
|
||||
|
||||
@@ -580,12 +589,12 @@ msgstr "Créer un nouveau {0}"
|
||||
msgid "Create new key"
|
||||
msgstr "Créer une nouvelle clé"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Créer une nouvelle étiquette"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Créer une nouvelle liste"
|
||||
|
||||
@@ -743,6 +752,10 @@ msgstr "Nom d'affichage mis à jour"
|
||||
msgid "Do you offer a free plan?"
|
||||
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
|
||||
msgid "docs"
|
||||
msgstr "documentation"
|
||||
@@ -769,8 +782,12 @@ msgstr "Vous n'avez pas de compte ? <0><1>Inscrivez-vous</1></0>"
|
||||
msgid "Done"
|
||||
msgstr "Fait"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Échec du téléchargement"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Modifier l'URL du tableau"
|
||||
|
||||
@@ -782,7 +799,7 @@ msgstr "Modifier le commentaire"
|
||||
msgid "Edit label"
|
||||
msgstr "Modifier l'étiquette"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Modifier l'URL de l'espace de travail"
|
||||
|
||||
@@ -790,7 +807,7 @@ msgstr "Modifier l'URL de l'espace de travail"
|
||||
msgid "Editing"
|
||||
msgstr "Édition"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "e-mail"
|
||||
|
||||
@@ -810,11 +827,11 @@ msgstr "Saisissez votre mot de passe actuel"
|
||||
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é."
|
||||
|
||||
#: src/components/AuthForm.tsx:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Saisissez votre adresse e-mail"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Saisissez votre nom"
|
||||
|
||||
@@ -822,7 +839,7 @@ msgstr "Saisissez votre nom"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Saisissez votre nouveau mot de passe"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Saisissez votre mot de passe"
|
||||
|
||||
@@ -931,6 +948,11 @@ msgstr "Échec de la création du tableau"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Échec de connexion avec {0}. Veuillez réessayer."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Échec du téléversement de la pièce jointe. Veuillez réessayer."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -974,7 +996,7 @@ msgstr "Commentaires"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Commentaires envoyés"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Filtrer"
|
||||
|
||||
@@ -1028,7 +1050,7 @@ msgstr "Commencer"
|
||||
msgid "Get started by creating a new {0}"
|
||||
msgstr "Commencez par créer un nouveau {0}"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Commencez par créer une nouvelle liste"
|
||||
|
||||
@@ -1242,9 +1264,9 @@ msgstr "Kanban réinventé"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Gardez à l'esprit que cette action est irréversible."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Étiquettes"
|
||||
|
||||
@@ -1294,18 +1316,22 @@ msgstr "Licence"
|
||||
msgid "Light"
|
||||
msgstr "Clair"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Lien copié"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "Liste"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "Nom de la liste"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Listes"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Connexion | kan.bn"
|
||||
@@ -1326,7 +1352,7 @@ msgstr "Adoré par les équipes du monde entier"
|
||||
msgid "Low Priority"
|
||||
msgstr "Priorité basse"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "lien magique"
|
||||
|
||||
@@ -1348,9 +1374,9 @@ msgid "Medium Priority"
|
||||
msgstr "Priorité moyenne"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Membres"
|
||||
@@ -1403,7 +1429,7 @@ msgstr "Nouveau {0}"
|
||||
msgid "New API key"
|
||||
msgstr "Nouvelle clé API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "Nouvelle carte"
|
||||
|
||||
@@ -1419,8 +1445,8 @@ msgstr "Nouvel import"
|
||||
msgid "New label"
|
||||
msgstr "Nouvelle étiquette"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "Nouvelle liste"
|
||||
|
||||
@@ -1457,6 +1483,10 @@ msgstr "Prochaines étapes"
|
||||
msgid "No {0}"
|
||||
msgstr "Aucun {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "Aucune méthode d'authentification n'est actuellement disponible"
|
||||
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:242
|
||||
msgid "No boards found"
|
||||
msgstr "Aucun tableau trouvé"
|
||||
@@ -1465,7 +1495,11 @@ msgstr "Aucun tableau trouvé"
|
||||
msgid "No credit card required"
|
||||
msgstr "Aucune carte de crédit requise"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "No download URL available for this attachment."
|
||||
msgstr "Aucune URL de téléchargement disponible pour cette pièce jointe."
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
msgid "No lists"
|
||||
msgstr "Aucune liste"
|
||||
|
||||
@@ -1497,7 +1531,7 @@ msgstr "Open source"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Amis Open Source"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "ou"
|
||||
|
||||
@@ -1579,15 +1613,15 @@ msgstr "Plateforme"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Veuillez confirmer votre nouveau mot de passe"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Veuillez saisir une adresse e-mail valide"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Veuillez saisir un nom valide"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Veuillez saisir un mot de passe valide"
|
||||
|
||||
@@ -1598,15 +1632,16 @@ msgstr "Veuillez sélectionner un fichier à télécharger."
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1620,7 +1655,7 @@ msgstr "Veuillez sélectionner un fichier à télécharger."
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1664,7 +1699,7 @@ msgstr "Support par e-mail prioritaire"
|
||||
msgid "Privacy policy"
|
||||
msgstr "Politique de confidentialité"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Privé"
|
||||
|
||||
@@ -1684,7 +1719,7 @@ msgstr "Image de profil mise à jour"
|
||||
msgid "Profile picture"
|
||||
msgstr "Photo de profil"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Public"
|
||||
|
||||
@@ -1886,7 +1921,7 @@ msgstr "Inscription désactivée"
|
||||
msgid "Sign up is currently disabled. Please try again later."
|
||||
msgstr "L'inscription est actuellement désactivée. Veuillez réessayer plus tard."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "S'inscrire avec "
|
||||
|
||||
@@ -1953,8 +1988,8 @@ msgstr "Forfait d'équipe"
|
||||
msgid "Teams"
|
||||
msgstr "Équipes"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
@@ -2004,7 +2039,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "L'alternative open source <0/> à Trello"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "La visibilité de votre tableau a été définie sur {0}."
|
||||
|
||||
@@ -2028,11 +2063,11 @@ msgstr "Cette action ne peut pas être annulée."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "Cette clé API ne sera affichée qu'une seule fois. Veuillez la sauvegarder dans un emplacement sécurisé."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "Ce tableau est privé ou n'existe pas"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "Cette URL de tableau est déjà utilisée"
|
||||
|
||||
@@ -2102,7 +2137,7 @@ msgstr "Impossible d'ajouter l'élément à la liste de contrôle"
|
||||
msgid "Unable to add comment"
|
||||
msgstr "Impossible d'ajouter un commentaire"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "Impossible de créer la carte"
|
||||
|
||||
@@ -2110,7 +2145,7 @@ msgstr "Impossible de créer la carte"
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "Impossible de créer la liste de contrôle"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "Impossible de créer une liste"
|
||||
|
||||
@@ -2123,6 +2158,10 @@ msgstr "Impossible de créer le modèle"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "Impossible de créer l'espace de travail"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "Impossible de supprimer la pièce jointe"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "Impossible de supprimer la carte"
|
||||
@@ -2147,16 +2186,16 @@ msgstr "Impossible de supprimer le membre"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "Impossible d'envoyer les commentaires"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "Impossible de mettre à jour l'URL du tableau"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "Impossible de mettre à jour la visibilité du tableau"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "Impossible de mettre à jour la carte"
|
||||
|
||||
@@ -2176,7 +2215,7 @@ msgstr "Impossible de mettre à jour le commentaire"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "Impossible de mettre à jour les étiquettes"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "Impossible de mettre à jour la liste"
|
||||
@@ -2232,7 +2271,12 @@ msgstr "Membres illimités"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Espaces de travail illimités"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: 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/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2271,6 +2315,11 @@ msgstr "Passer à Pro"
|
||||
msgid "Upgrade to Pro ($29/month)"
|
||||
msgstr "Passer à Pro (29 $/mois)"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Échec du téléversement"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2320,7 +2369,7 @@ msgstr "Consultez et gérez votre facturation et votre abonnement."
|
||||
msgid "View docs"
|
||||
msgstr "Voir la documentation"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "Affichage uniquement"
|
||||
|
||||
@@ -2328,11 +2377,11 @@ msgstr "Affichage uniquement"
|
||||
msgid "View our roadmap."
|
||||
msgstr "Consultez notre feuille de route."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "Voir l'espace de travail"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Visibilité"
|
||||
|
||||
@@ -2340,6 +2389,10 @@ msgstr "Visibilité"
|
||||
msgid "We are using the <0>AGPL-3.0 license</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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "Nous ne faisons que commencer. "
|
||||
@@ -2369,10 +2422,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "Pourquoi créer un Trello open source ?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Espace de travail"
|
||||
@@ -2478,6 +2531,10 @@ msgstr "Vous vous êtes connecté avec succès."
|
||||
msgid "You have been signed up successfully."
|
||||
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
|
||||
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 !"
|
||||
@@ -2502,6 +2559,10 @@ msgstr "Vos tableaux ont été importés."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Votre nom d'affichage a été mis à jour."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Votre fichier a été téléversé avec succès."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "Votre mot de passe a été modifié."
|
||||
@@ -2518,6 +2579,10 @@ msgstr "Votre compte Trello a été déconnecté."
|
||||
msgid "Your Trello account is connected."
|
||||
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
|
||||
msgid "Your workspace description has been updated."
|
||||
msgstr "La description de votre espace de travail a été mise à jour."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,17 +31,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} etichette"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} non trovato"
|
||||
|
||||
@@ -79,8 +79,8 @@ msgstr "Account"
|
||||
msgid "Account deleted"
|
||||
msgstr "Account eliminato"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Attività"
|
||||
|
||||
@@ -109,7 +109,7 @@ msgstr "Aggiungi un elemento..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Aggiungi checklist"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Aggiungi descrizione... (digita '/' per aprire i comandi o '@' per menzionare)"
|
||||
|
||||
@@ -270,6 +270,10 @@ msgstr "ha assegnato <0>{0}</0> alla scheda"
|
||||
msgid "Assignees"
|
||||
msgstr "Assegnatari"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Allegato caricato"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "In attesa del cliente"
|
||||
@@ -304,9 +308,9 @@ msgstr "Portale di fatturazione"
|
||||
msgid "Blog Post"
|
||||
msgstr "Post del blog"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Bacheca"
|
||||
|
||||
@@ -323,7 +327,7 @@ msgstr "Il nome della bacheca non può superare i 100 caratteri"
|
||||
msgid "Board name is required"
|
||||
msgstr "Il nome della bacheca è obbligatorio"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Bacheca non trovata"
|
||||
|
||||
@@ -331,19 +335,19 @@ msgstr "Bacheca non trovata"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Condivisione bacheca & inviti"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "L'URL della bacheca può contenere solo lettere, numeri e trattini"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "L'URL della bacheca non può superare i 60 caratteri"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "URL della bacheca copiato negli appunti"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "L'URL della bacheca deve contenere almeno 3 caratteri"
|
||||
|
||||
@@ -352,7 +356,7 @@ msgstr "L'URL della bacheca deve contenere almeno 3 caratteri"
|
||||
msgid "Board visibility"
|
||||
msgstr "Visibilità bacheca"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Visibilità della bacheca aggiornata"
|
||||
|
||||
@@ -387,15 +391,16 @@ msgstr "Segnalazione bug"
|
||||
msgid "Cancel"
|
||||
msgstr "Annulla"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Carta"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Carta non trovata"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Titolo della carta"
|
||||
|
||||
@@ -428,7 +433,7 @@ msgstr "Nome della checklist"
|
||||
msgid "Checklists"
|
||||
msgstr "Checklist"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Cancella filtri"
|
||||
|
||||
@@ -489,6 +494,10 @@ msgstr "ha completato un elemento della checklist"
|
||||
msgid "completed checklist item <0>{0}</0>"
|
||||
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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Conferma la tua nuova password"
|
||||
@@ -519,12 +528,12 @@ msgstr "Contattaci"
|
||||
msgid "Content Creation"
|
||||
msgstr "Creazione contenuti"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Continua con "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Continua con {0}"
|
||||
|
||||
@@ -542,8 +551,8 @@ msgid "Create {0}"
|
||||
msgstr "Crea {0}"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Crea un altro"
|
||||
|
||||
@@ -551,7 +560,7 @@ msgstr "Crea un altro"
|
||||
msgid "Create API key"
|
||||
msgstr "Crea chiave API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Crea carta"
|
||||
|
||||
@@ -567,7 +576,7 @@ msgstr "Crea link di invito"
|
||||
msgid "Create label"
|
||||
msgstr "Crea etichetta"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Crea lista"
|
||||
|
||||
@@ -580,12 +589,12 @@ msgstr "Crea nuovo {0}"
|
||||
msgid "Create new key"
|
||||
msgstr "Crea nuova chiave"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Crea nuova etichetta"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Crea nuova lista"
|
||||
|
||||
@@ -743,6 +752,10 @@ msgstr "Nome visualizzato aggiornato"
|
||||
msgid "Do you offer a free plan?"
|
||||
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
|
||||
msgid "docs"
|
||||
msgstr "documenti"
|
||||
@@ -769,8 +782,12 @@ msgstr "Non hai un account? <0><1>Registrati</1></0>"
|
||||
msgid "Done"
|
||||
msgstr "Fatto"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Download fallito"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Modifica URL della bacheca"
|
||||
|
||||
@@ -782,7 +799,7 @@ msgstr "Modifica commento"
|
||||
msgid "Edit label"
|
||||
msgstr "Modifica etichetta"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Modifica URL dell'area di lavoro"
|
||||
|
||||
@@ -790,7 +807,7 @@ msgstr "Modifica URL dell'area di lavoro"
|
||||
msgid "Editing"
|
||||
msgstr "Modifica"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "email"
|
||||
|
||||
@@ -810,11 +827,11 @@ msgstr "Inserisci la tua password attuale"
|
||||
msgid "Enter your current password and choose a new secure password."
|
||||
msgstr "Inserisci la tua password attuale e scegli una nuova password sicura."
|
||||
|
||||
#: src/components/AuthForm.tsx:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Inserisci il tuo indirizzo email"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Inserisci il tuo nome"
|
||||
|
||||
@@ -822,7 +839,7 @@ msgstr "Inserisci il tuo nome"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Inserisci la tua nuova password"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Inserisci la tua password"
|
||||
|
||||
@@ -931,6 +948,11 @@ msgstr "Impossibile creare la bacheca"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Accesso con {0} fallito. Riprova."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Impossibile caricare l'allegato. Riprova."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -974,7 +996,7 @@ msgstr "Feedback"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Feedback inviato"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Filtra"
|
||||
|
||||
@@ -1028,7 +1050,7 @@ msgstr "Inizia ora"
|
||||
msgid "Get started by creating a new {0}"
|
||||
msgstr "Inizia creando un nuovo {0}"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Inizia creando una nuova lista"
|
||||
|
||||
@@ -1242,9 +1264,9 @@ msgstr "Kanban reinventato"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Tieni presente che questa azione è irreversibile."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Etichette"
|
||||
|
||||
@@ -1294,18 +1316,22 @@ msgstr "Licenza"
|
||||
msgid "Light"
|
||||
msgstr "Chiaro"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Link copiato"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "Lista"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "Nome lista"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Liste"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Login | kan.bn"
|
||||
@@ -1326,7 +1352,7 @@ msgstr "Amato dai team di tutto il mondo"
|
||||
msgid "Low Priority"
|
||||
msgstr "Bassa priorità"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "link magico"
|
||||
|
||||
@@ -1348,9 +1374,9 @@ msgid "Medium Priority"
|
||||
msgstr "Media priorità"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Membri"
|
||||
@@ -1403,7 +1429,7 @@ msgstr "Nuovo {0}"
|
||||
msgid "New API key"
|
||||
msgstr "Nuova chiave API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "Nuova carta"
|
||||
|
||||
@@ -1419,8 +1445,8 @@ msgstr "Nuova importazione"
|
||||
msgid "New label"
|
||||
msgstr "Nuova etichetta"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "Nuova lista"
|
||||
|
||||
@@ -1457,6 +1483,10 @@ msgstr "Prossimi passi"
|
||||
msgid "No {0}"
|
||||
msgstr "Nessun {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "Nessun metodo di autenticazione è attualmente disponibile"
|
||||
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:242
|
||||
msgid "No boards found"
|
||||
msgstr "Nessuna bacheca trovata"
|
||||
@@ -1465,7 +1495,11 @@ msgstr "Nessuna bacheca trovata"
|
||||
msgid "No credit card required"
|
||||
msgstr "Nessuna carta di credito richiesta"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "No download URL available for this attachment."
|
||||
msgstr "Nessun URL di download disponibile per questo allegato."
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
msgid "No lists"
|
||||
msgstr "Nessuna lista"
|
||||
|
||||
@@ -1497,7 +1531,7 @@ msgstr "Open source"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Amici Open Source"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "o"
|
||||
|
||||
@@ -1579,15 +1613,15 @@ msgstr "Piattaforma"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Conferma la tua nuova password"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Inserisci un indirizzo email valido"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Inserisci un nome valido"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Inserisci una password valida"
|
||||
|
||||
@@ -1598,15 +1632,16 @@ msgstr "Seleziona un file da caricare."
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1620,7 +1655,7 @@ msgstr "Seleziona un file da caricare."
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1664,7 +1699,7 @@ msgstr "Supporto email prioritario"
|
||||
msgid "Privacy policy"
|
||||
msgstr "Informativa sulla privacy"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Privato"
|
||||
|
||||
@@ -1684,7 +1719,7 @@ msgstr "Immagine del profilo aggiornata"
|
||||
msgid "Profile picture"
|
||||
msgstr "Immagine del profilo"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Pubblico"
|
||||
|
||||
@@ -1886,7 +1921,7 @@ msgstr "Registrazione disabilitata"
|
||||
msgid "Sign up is currently disabled. Please try again later."
|
||||
msgstr "La registrazione è attualmente disabilitata. Riprova più tardi."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "Registrati con "
|
||||
|
||||
@@ -1953,8 +1988,8 @@ msgstr "Piano Team"
|
||||
msgid "Teams"
|
||||
msgstr "Team"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Template"
|
||||
|
||||
@@ -2004,7 +2039,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "L'alternativa open source <0/> a Trello"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "La visibilità della tua bacheca è stata impostata su {0}."
|
||||
|
||||
@@ -2028,11 +2063,11 @@ msgstr "Questa azione non può essere annullata."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "Questa chiave API verrà mostrata una sola volta. Salvala in un luogo sicuro."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "Questa bacheca è privata o non esiste"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "Questo URL della bacheca è già stato utilizzato"
|
||||
|
||||
@@ -2102,7 +2137,7 @@ msgstr "Impossibile aggiungere l'elemento della checklist"
|
||||
msgid "Unable to add comment"
|
||||
msgstr "Impossibile aggiungere il commento"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "Impossibile creare la scheda"
|
||||
|
||||
@@ -2110,7 +2145,7 @@ msgstr "Impossibile creare la scheda"
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "Impossibile creare la checklist"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "Impossibile creare la lista"
|
||||
|
||||
@@ -2123,6 +2158,10 @@ msgstr "Impossibile creare il template"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "Impossibile creare lo spazio di lavoro"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "Impossibile eliminare l'allegato"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "Impossibile eliminare la scheda"
|
||||
@@ -2147,16 +2186,16 @@ msgstr "Impossibile rimuovere il membro"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "Impossibile inviare feedback"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "Impossibile aggiornare l'URL della bacheca"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "Impossibile aggiornare la visibilità della bacheca"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "Impossibile aggiornare la scheda"
|
||||
|
||||
@@ -2176,7 +2215,7 @@ msgstr "Impossibile aggiornare il commento"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "Impossibile aggiornare le etichette"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "Impossibile aggiornare la lista"
|
||||
@@ -2232,7 +2271,12 @@ msgstr "Membri illimitati"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Aree di lavoro illimitate"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: 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/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2271,6 +2315,11 @@ msgstr "Passa a Pro"
|
||||
msgid "Upgrade to Pro ($29/month)"
|
||||
msgstr "Passa a Pro ($29/mese)"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Caricamento fallito"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2320,7 +2369,7 @@ msgstr "Visualizza e gestisci la tua fatturazione e abbonamento."
|
||||
msgid "View docs"
|
||||
msgstr "Visualizza documenti"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "Solo visualizzazione"
|
||||
|
||||
@@ -2328,11 +2377,11 @@ msgstr "Solo visualizzazione"
|
||||
msgid "View our roadmap."
|
||||
msgstr "Visualizza la nostra roadmap."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "Visualizza spazio di lavoro"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Visibilità"
|
||||
|
||||
@@ -2340,6 +2389,10 @@ msgstr "Visibilità"
|
||||
msgid "We are using the <0>AGPL-3.0 license</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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "Siamo solo all'inizio. "
|
||||
@@ -2369,10 +2422,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "Perché creare un Trello open source?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Spazio di lavoro"
|
||||
@@ -2478,6 +2531,10 @@ msgstr "Hai effettuato l'accesso con successo."
|
||||
msgid "You have been signed up successfully."
|
||||
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
|
||||
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!"
|
||||
@@ -2502,6 +2559,10 @@ msgstr "Le tue bacheche sono state importate."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Il tuo nome visualizzato è stato aggiornato."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Il tuo file è stato caricato con successo."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "La tua password è stata modificata."
|
||||
@@ -2518,6 +2579,10 @@ msgstr "Il tuo account Trello è stato disconnesso."
|
||||
msgid "Your Trello account is connected."
|
||||
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
|
||||
msgid "Your workspace description has been updated."
|
||||
msgstr "La descrizione della tua area di lavoro è stata aggiornata."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,17 +31,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} labels"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} niet gevonden"
|
||||
|
||||
@@ -79,8 +79,8 @@ msgstr "Account"
|
||||
msgid "Account deleted"
|
||||
msgstr "Account verwijderd"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Activiteit"
|
||||
|
||||
@@ -109,7 +109,7 @@ msgstr "Voeg een item toe..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Checklist toevoegen"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Beschrijving toevoegen... (typ '/' om commando's te openen of '@' om te vermelden)"
|
||||
|
||||
@@ -270,6 +270,10 @@ msgstr "heeft <0>{0}</0> aan de kaart toegewezen"
|
||||
msgid "Assignees"
|
||||
msgstr "Toegewezen personen"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Bijlage geüpload"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "Wachten op klant"
|
||||
@@ -304,9 +308,9 @@ msgstr "Factureringsportaal"
|
||||
msgid "Blog Post"
|
||||
msgstr "Blogbericht"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Bord"
|
||||
|
||||
@@ -323,7 +327,7 @@ msgstr "Bordnaam mag niet langer zijn dan 100 tekens"
|
||||
msgid "Board name is required"
|
||||
msgstr "Bordnaam is verplicht"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Bord niet gevonden"
|
||||
|
||||
@@ -331,19 +335,19 @@ msgstr "Bord niet gevonden"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Bord delen & uitnodigingen"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "Board URL kan alleen letters, cijfers en koppeltekens bevatten"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "Board URL mag niet langer zijn dan 60 tekens"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "Board URL gekopieerd naar klembord"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "Board URL moet minimaal 3 tekens lang zijn"
|
||||
|
||||
@@ -352,7 +356,7 @@ msgstr "Board URL moet minimaal 3 tekens lang zijn"
|
||||
msgid "Board visibility"
|
||||
msgstr "Bordzichtbaarheid"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Zichtbaarheid van bord bijgewerkt"
|
||||
|
||||
@@ -387,15 +391,16 @@ msgstr "Bugrapport"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Kaart"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Kaart niet gevonden"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Kaarttitel"
|
||||
|
||||
@@ -428,7 +433,7 @@ msgstr "Naam checklist"
|
||||
msgid "Checklists"
|
||||
msgstr "Checklists"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Filters wissen"
|
||||
|
||||
@@ -489,6 +494,10 @@ msgstr "heeft een checklistitem voltooid"
|
||||
msgid "completed checklist item <0>{0}</0>"
|
||||
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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Bevestig je nieuwe wachtwoord"
|
||||
@@ -519,12 +528,12 @@ msgstr "Neem contact op"
|
||||
msgid "Content Creation"
|
||||
msgstr "Content creatie"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Doorgaan met "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Doorgaan met {0}"
|
||||
|
||||
@@ -542,8 +551,8 @@ msgid "Create {0}"
|
||||
msgstr "{0} maken"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Maak nog een"
|
||||
|
||||
@@ -551,7 +560,7 @@ msgstr "Maak nog een"
|
||||
msgid "Create API key"
|
||||
msgstr "API-sleutel aanmaken"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Maak kaart"
|
||||
|
||||
@@ -567,7 +576,7 @@ msgstr "Uitnodigingslink maken"
|
||||
msgid "Create label"
|
||||
msgstr "Maak label"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Lijst maken"
|
||||
|
||||
@@ -580,12 +589,12 @@ msgstr "Nieuwe {0} maken"
|
||||
msgid "Create new key"
|
||||
msgstr "Nieuwe sleutel aanmaken"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Maak nieuw label"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Maak nieuwe lijst"
|
||||
|
||||
@@ -743,6 +752,10 @@ msgstr "Weergavenaam bijgewerkt"
|
||||
msgid "Do you offer a free plan?"
|
||||
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
|
||||
msgid "docs"
|
||||
msgstr "docs"
|
||||
@@ -769,8 +782,12 @@ msgstr "Heb je geen account? <0><1>Registreer je</1></0>"
|
||||
msgid "Done"
|
||||
msgstr "Klaar"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Download mislukt"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Board URL bewerken"
|
||||
|
||||
@@ -782,7 +799,7 @@ msgstr "Reactie bewerken"
|
||||
msgid "Edit label"
|
||||
msgstr "Label bewerken"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Werkruimte-URL bewerken"
|
||||
|
||||
@@ -790,7 +807,7 @@ msgstr "Werkruimte-URL bewerken"
|
||||
msgid "Editing"
|
||||
msgstr "Bewerken"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "e-mail"
|
||||
|
||||
@@ -810,11 +827,11 @@ msgstr "Voer je huidige wachtwoord in"
|
||||
msgid "Enter your current password and choose a new secure password."
|
||||
msgstr "Voer je huidige wachtwoord in en kies een nieuw veilig wachtwoord."
|
||||
|
||||
#: src/components/AuthForm.tsx:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Voer je e-mailadres in"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Voer je naam in"
|
||||
|
||||
@@ -822,7 +839,7 @@ msgstr "Voer je naam in"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Voer je nieuwe wachtwoord in"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Voer je wachtwoord in"
|
||||
|
||||
@@ -931,6 +948,11 @@ msgstr "Bord maken mislukt"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Inloggen met {0} is mislukt. Probeer het opnieuw."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Bijlage uploaden mislukt. Probeer het opnieuw."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -974,7 +996,7 @@ msgstr "Feedback"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Feedback verzonden"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
|
||||
@@ -1028,7 +1050,7 @@ msgstr "Aan de slag"
|
||||
msgid "Get started by creating a new {0}"
|
||||
msgstr "Begin door een nieuwe {0} te maken"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Begin door een nieuwe lijst te maken"
|
||||
|
||||
@@ -1242,9 +1264,9 @@ msgstr "Kanban opnieuw uitgevonden"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Houd er rekening mee dat deze actie onomkeerbaar is."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Labels"
|
||||
|
||||
@@ -1294,18 +1316,22 @@ msgstr "Licentie"
|
||||
msgid "Light"
|
||||
msgstr "Licht"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Link gekopieerd"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "Lijst"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "Lijstnaam"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Lijsten"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Login | kan.bn"
|
||||
@@ -1326,7 +1352,7 @@ msgstr "Geliefd door teams wereldwijd"
|
||||
msgid "Low Priority"
|
||||
msgstr "Lage prioriteit"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "magische link"
|
||||
|
||||
@@ -1348,9 +1374,9 @@ msgid "Medium Priority"
|
||||
msgstr "Gemiddelde prioriteit"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Leden"
|
||||
@@ -1403,7 +1429,7 @@ msgstr "Nieuwe {0}"
|
||||
msgid "New API key"
|
||||
msgstr "Nieuwe API-sleutel"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "Nieuwe kaart"
|
||||
|
||||
@@ -1419,8 +1445,8 @@ msgstr "Nieuwe import"
|
||||
msgid "New label"
|
||||
msgstr "Nieuw label"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "Nieuwe lijst"
|
||||
|
||||
@@ -1457,6 +1483,10 @@ msgstr "Volgende stappen"
|
||||
msgid "No {0}"
|
||||
msgstr "Geen {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "Er zijn momenteel geen authenticatiemethoden beschikbaar"
|
||||
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:242
|
||||
msgid "No boards found"
|
||||
msgstr "Geen borden gevonden"
|
||||
@@ -1465,7 +1495,11 @@ msgstr "Geen borden gevonden"
|
||||
msgid "No credit card required"
|
||||
msgstr "Geen creditcard vereist"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "No download URL available for this attachment."
|
||||
msgstr "Geen download-URL beschikbaar voor deze bijlage."
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
msgid "No lists"
|
||||
msgstr "Geen lijsten"
|
||||
|
||||
@@ -1497,7 +1531,7 @@ msgstr "Open source"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Open Source Vrienden"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "of"
|
||||
|
||||
@@ -1579,15 +1613,15 @@ msgstr "Platform"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Bevestig je nieuwe wachtwoord"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Voer een geldig e-mailadres in"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Voer een geldige naam in"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Voer een geldig wachtwoord in"
|
||||
|
||||
@@ -1598,15 +1632,16 @@ msgstr "Selecteer een bestand om te uploaden."
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1620,7 +1655,7 @@ msgstr "Selecteer een bestand om te uploaden."
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1664,7 +1699,7 @@ msgstr "Prioriteit e-mailondersteuning"
|
||||
msgid "Privacy policy"
|
||||
msgstr "Privacybeleid"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Privé"
|
||||
|
||||
@@ -1684,7 +1719,7 @@ msgstr "Profielafbeelding bijgewerkt"
|
||||
msgid "Profile picture"
|
||||
msgstr "Profielfoto"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Openbaar"
|
||||
|
||||
@@ -1886,7 +1921,7 @@ msgstr "Registreren uitgeschakeld"
|
||||
msgid "Sign up is currently disabled. Please try again later."
|
||||
msgstr "Registreren is momenteel uitgeschakeld. Probeer het later opnieuw."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "Registreren met "
|
||||
|
||||
@@ -1953,8 +1988,8 @@ msgstr "Teamplan"
|
||||
msgid "Teams"
|
||||
msgstr "Teams"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Sjabloon"
|
||||
|
||||
@@ -2004,7 +2039,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "Het open source <0/> alternatief voor Trello"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "De zichtbaarheid van je bord is ingesteld op {0}."
|
||||
|
||||
@@ -2028,11 +2063,11 @@ msgstr "Deze actie kan niet ongedaan worden gemaakt."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "Deze API-sleutel wordt slechts één keer getoond. Bewaar deze op een veilige plaats."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "Dit bord is privé of bestaat niet"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "Deze board URL is al in gebruik"
|
||||
|
||||
@@ -2102,7 +2137,7 @@ msgstr "Kan checklistitem niet toevoegen"
|
||||
msgid "Unable to add comment"
|
||||
msgstr "Kan geen reactie toevoegen"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "Kan kaart niet aanmaken"
|
||||
|
||||
@@ -2110,7 +2145,7 @@ msgstr "Kan kaart niet aanmaken"
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "Kan checklist niet maken"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "Kan geen lijst maken"
|
||||
|
||||
@@ -2123,6 +2158,10 @@ msgstr "Kan sjabloon niet maken"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "Kan werkruimte niet aanmaken"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "Kan bijlage niet verwijderen"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "Kan kaart niet verwijderen"
|
||||
@@ -2147,16 +2186,16 @@ msgstr "Kan lid niet verwijderen"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "Feedback kon niet worden verzonden"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "Kan board URL niet bijwerken"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "Kan bordzichtbaarheid niet bijwerken"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "Kan kaart niet bijwerken"
|
||||
|
||||
@@ -2176,7 +2215,7 @@ msgstr "Kan reactie niet bijwerken"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "Kan labels niet bijwerken"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "Kan lijst niet bijwerken"
|
||||
@@ -2232,7 +2271,12 @@ msgstr "Onbeperkt aantal leden"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Onbeperkte werkruimtes"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: 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/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2271,6 +2315,11 @@ msgstr "Upgraden naar Pro"
|
||||
msgid "Upgrade to Pro ($29/month)"
|
||||
msgstr "Upgrade naar Pro ($29/maand)"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Uploaden mislukt"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2320,7 +2369,7 @@ msgstr "Bekijk en beheer je facturering en abonnement."
|
||||
msgid "View docs"
|
||||
msgstr "Bekijk documentatie"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "Alleen bekijken"
|
||||
|
||||
@@ -2328,11 +2377,11 @@ msgstr "Alleen bekijken"
|
||||
msgid "View our roadmap."
|
||||
msgstr "Bekijk onze roadmap."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "Werkruimte bekijken"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Zichtbaarheid"
|
||||
|
||||
@@ -2340,6 +2389,10 @@ msgstr "Zichtbaarheid"
|
||||
msgid "We are using the <0>AGPL-3.0 license</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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "We zijn nog maar net begonnen. "
|
||||
@@ -2369,10 +2422,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "Waarom een open source Trello maken?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Werkruimte"
|
||||
@@ -2478,6 +2531,10 @@ msgstr "Je bent succesvol ingelogd."
|
||||
msgid "You have been signed up successfully."
|
||||
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
|
||||
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!"
|
||||
@@ -2502,6 +2559,10 @@ msgstr "Je borden zijn geïmporteerd."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Je weergavenaam is bijgewerkt."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Je bestand is succesvol geüpload."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "Je wachtwoord is gewijzigd."
|
||||
@@ -2518,6 +2579,10 @@ msgstr "Je Trello-account is ontkoppeld."
|
||||
msgid "Your Trello account is connected."
|
||||
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
|
||||
msgid "Your workspace description has been updated."
|
||||
msgstr "Je werkruimtebeschrijving is bijgewerkt."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -36,17 +36,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} etykiety"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} nie znaleziono"
|
||||
|
||||
@@ -84,8 +84,8 @@ msgstr "Konto"
|
||||
msgid "Account deleted"
|
||||
msgstr "Konto zostało usunięte"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Aktywność"
|
||||
|
||||
@@ -114,7 +114,7 @@ msgstr "Dodaj element..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Dodaj listę kontrolną"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Dodaj opis... (wpisz '/' aby otworzyć polecenia lub '@' aby wspomnieć)"
|
||||
|
||||
@@ -275,6 +275,10 @@ msgstr "przypisano <0>{0}</0> do karty"
|
||||
msgid "Assignees"
|
||||
msgstr "Osoby przypisane"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Załącznik został przesłany"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "Oczekuje na klienta"
|
||||
@@ -309,9 +313,9 @@ msgstr "Portal rozliczeniowy"
|
||||
msgid "Blog Post"
|
||||
msgstr "Post na blogu"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Tablica"
|
||||
|
||||
@@ -328,7 +332,7 @@ msgstr "Nazwa tablicy nie może przekraczać 100 znaków"
|
||||
msgid "Board name is required"
|
||||
msgstr "Nazwa tablicy jest wymagana"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Tablica nie została znaleziona"
|
||||
|
||||
@@ -336,19 +340,19 @@ msgstr "Tablica nie została znaleziona"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Udostępnianie tablicy i zaproszenia"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "Adres URL tablicy może zawierać tylko litery, cyfry i myślniki"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "Adres URL tablicy nie może przekraczać 60 znaków"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "Adres URL tablicy został skopiowany do schowka"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "Adres URL tablicy musi mieć co najmniej 3 znaki"
|
||||
|
||||
@@ -357,7 +361,7 @@ msgstr "Adres URL tablicy musi mieć co najmniej 3 znaki"
|
||||
msgid "Board visibility"
|
||||
msgstr "Widoczność tablicy"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Widoczność tablicy została zaktualizowana"
|
||||
|
||||
@@ -392,15 +396,16 @@ msgstr "Zgłoszenie błędu"
|
||||
msgid "Cancel"
|
||||
msgstr "Anuluj"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Karta"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Nie można było znaleźć karty"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Tytuł karty"
|
||||
|
||||
@@ -433,7 +438,7 @@ msgstr "Nazwa listy kontrolnej"
|
||||
msgid "Checklists"
|
||||
msgstr "Listy kontrolne"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Wyczyść filtry"
|
||||
|
||||
@@ -494,6 +499,10 @@ msgstr "ukończono element listy kontrolnej"
|
||||
msgid "completed checklist item <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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Potwierdź swoje nowe hasło"
|
||||
@@ -524,12 +533,12 @@ msgstr "Skontaktuj się z nami"
|
||||
msgid "Content Creation"
|
||||
msgstr "Tworzenie treści"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Kontynuuj z "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Kontynuuj z {0}"
|
||||
|
||||
@@ -547,8 +556,8 @@ msgid "Create {0}"
|
||||
msgstr "Utwórz {0}"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Utwórz kolejny"
|
||||
|
||||
@@ -556,7 +565,7 @@ msgstr "Utwórz kolejny"
|
||||
msgid "Create API key"
|
||||
msgstr "Utwórz klucz API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Utwórz kartę"
|
||||
|
||||
@@ -572,7 +581,7 @@ msgstr "Utwórz link zaproszenia"
|
||||
msgid "Create label"
|
||||
msgstr "Utwórz etykietę"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Utwórz listę"
|
||||
|
||||
@@ -585,12 +594,12 @@ msgstr "Utwórz nowy {0}"
|
||||
msgid "Create new key"
|
||||
msgstr "Utwórz nowy klucz"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Utwórz nową etykietę"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Utwórz nową listę"
|
||||
|
||||
@@ -748,6 +757,10 @@ msgstr "Zaktualizowano wyświetlaną nazwę"
|
||||
msgid "Do you offer a free 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
|
||||
msgid "docs"
|
||||
msgstr "dokumentacja"
|
||||
@@ -774,8 +787,12 @@ msgstr "Nie masz konta? <0><1>Zarejestruj się</1></0>"
|
||||
msgid "Done"
|
||||
msgstr "Gotowe"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Pobieranie nie powiodło się"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Edytuj URL tablicy"
|
||||
|
||||
@@ -787,7 +804,7 @@ msgstr "Edytuj komentarz"
|
||||
msgid "Edit label"
|
||||
msgstr "Edytuj etykietę"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Edytuj URL przestrzeni roboczej"
|
||||
|
||||
@@ -795,7 +812,7 @@ msgstr "Edytuj URL przestrzeni roboczej"
|
||||
msgid "Editing"
|
||||
msgstr "Edycja"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "email"
|
||||
|
||||
@@ -815,11 +832,11 @@ msgstr "Wprowadź swoje obecne hasło"
|
||||
msgid "Enter your current password and choose a new secure password."
|
||||
msgstr "Wprowadź swoje obecne hasło i wybierz nowe, bezpieczne hasło."
|
||||
|
||||
#: src/components/AuthForm.tsx:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Wprowadź swój adres e-mail"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Wprowadź swoje imię"
|
||||
|
||||
@@ -827,7 +844,7 @@ msgstr "Wprowadź swoje imię"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Wprowadź swoje nowe hasło"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Wprowadź swoje hasło"
|
||||
|
||||
@@ -936,6 +953,11 @@ msgstr "Nie udało się utworzyć tablicy"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Nie udało się zalogować za pomocą {0}. Spróbuj ponownie."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Nie udało się przesłać załącznika. Proszę spróbować ponownie."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -979,7 +1001,7 @@ msgstr "Opinie"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Opinia wysłana"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Filtr"
|
||||
|
||||
@@ -1033,7 +1055,7 @@ msgstr "Rozpocznij"
|
||||
msgid "Get started by creating a new {0}"
|
||||
msgstr "Rozpocznij, tworząc nowy {0}"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Rozpocznij, tworząc nową listę"
|
||||
|
||||
@@ -1247,9 +1269,9 @@ msgstr "Kanban wymyślony na nowo"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Pamiętaj, że ta akcja jest nieodwracalna."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Etykiety"
|
||||
|
||||
@@ -1299,18 +1321,22 @@ msgstr "Licencja"
|
||||
msgid "Light"
|
||||
msgstr "Jasny"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Link skopiowany"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "Lista"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "Nazwa listy"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Listy"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Zaloguj się | kan.bn"
|
||||
@@ -1331,7 +1357,7 @@ msgstr "Uwielbiane przez zespoły na całym świecie"
|
||||
msgid "Low Priority"
|
||||
msgstr "Niski priorytet"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "magiczny link"
|
||||
|
||||
@@ -1353,9 +1379,9 @@ msgid "Medium Priority"
|
||||
msgstr "Średni priorytet"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Członkowie"
|
||||
@@ -1408,7 +1434,7 @@ msgstr "Nowy {0}"
|
||||
msgid "New API key"
|
||||
msgstr "Nowy klucz API"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "Nowa karta"
|
||||
|
||||
@@ -1424,8 +1450,8 @@ msgstr "Nowy import"
|
||||
msgid "New label"
|
||||
msgstr "Nowa etykieta"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "Nowa lista"
|
||||
|
||||
@@ -1462,6 +1488,10 @@ msgstr "Następne kroki"
|
||||
msgid "No {0}"
|
||||
msgstr "Brak {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "Obecnie brak dostępnych metod uwierzytelniania"
|
||||
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:242
|
||||
msgid "No boards found"
|
||||
msgstr "Nie znaleziono tablic"
|
||||
@@ -1470,7 +1500,11 @@ msgstr "Nie znaleziono tablic"
|
||||
msgid "No credit card required"
|
||||
msgstr "Nie wymagana karta kredytowa"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "No download URL available for this attachment."
|
||||
msgstr "Brak dostępnego URL do pobrania tego załącznika."
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
msgid "No lists"
|
||||
msgstr "Brak list"
|
||||
|
||||
@@ -1502,7 +1536,7 @@ msgstr "Otwartoźródłowe"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Przyjaciele Open Source"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "lub"
|
||||
|
||||
@@ -1584,15 +1618,15 @@ msgstr "Platforma"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Proszę potwierdzić nowe hasło"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Proszę wprowadzić prawidłowy adres e-mail"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Proszę wprowadzić poprawne imię"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Proszę wprowadzić poprawne hasło"
|
||||
|
||||
@@ -1603,15 +1637,16 @@ msgstr "Proszę wybrać plik do przesłania."
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1625,7 +1660,7 @@ msgstr "Proszę wybrać plik do przesłania."
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1669,7 +1704,7 @@ msgstr "Priorytetowe wsparcie e-mail"
|
||||
msgid "Privacy policy"
|
||||
msgstr "Polityka prywatności"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Prywatne"
|
||||
|
||||
@@ -1689,7 +1724,7 @@ msgstr "Zaktualizowano zdjęcie profilowe"
|
||||
msgid "Profile picture"
|
||||
msgstr "Zdjęcie profilowe"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Publiczne"
|
||||
|
||||
@@ -1891,7 +1926,7 @@ msgstr "Rejestracja wyłączona"
|
||||
msgid "Sign up is currently disabled. Please try again later."
|
||||
msgstr "Rejestracja jest obecnie wyłączona. Spróbuj ponownie później."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "Zarejestruj się za pomocą "
|
||||
|
||||
@@ -1958,8 +1993,8 @@ msgstr "Plan zespołowy"
|
||||
msgid "Teams"
|
||||
msgstr "Zespoły"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Szablon"
|
||||
|
||||
@@ -2009,7 +2044,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "Otwarta alternatywa <0/> dla Trello"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "Widoczność Twojej tablicy została ustawiona na {0}."
|
||||
|
||||
@@ -2033,11 +2068,11 @@ msgstr "Tej akcji nie można cofnąć."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "Ten klucz API zostanie wyświetlony tylko raz. Proszę zapisać go w bezpiecznym miejscu."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "Ta tablica jest prywatna lub nie istnieje"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "Ten adres URL tablicy jest już zajęty"
|
||||
|
||||
@@ -2107,7 +2142,7 @@ msgstr "Nie można dodać elementu listy kontrolnej"
|
||||
msgid "Unable to add comment"
|
||||
msgstr "Nie można dodać komentarza"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "Nie można utworzyć karty"
|
||||
|
||||
@@ -2115,7 +2150,7 @@ msgstr "Nie można utworzyć karty"
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "Nie można utworzyć listy kontrolnej"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "Nie można utworzyć listy"
|
||||
|
||||
@@ -2128,6 +2163,10 @@ msgstr "Nie można utworzyć szablonu"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "Nie można utworzyć przestrzeni roboczej"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "Nie można usunąć załącznika"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "Nie można usunąć karty"
|
||||
@@ -2152,16 +2191,16 @@ msgstr "Nie można usunąć członka"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "Nie można wysłać opinii"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "Nie można zaktualizować adresu URL tablicy"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "Nie można zaktualizować widoczności tablicy"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "Nie można zaktualizować karty"
|
||||
|
||||
@@ -2181,7 +2220,7 @@ msgstr "Nie można zaktualizować komentarza"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "Nie można zaktualizować etykiet"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "Nie można zaktualizować listy"
|
||||
@@ -2237,7 +2276,12 @@ msgstr "Nieograniczona liczba członków"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Nieograniczona liczba przestrzeni roboczych"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: 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/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2276,6 +2320,11 @@ msgstr "Ulepsz do planu Pro"
|
||||
msgid "Upgrade to Pro ($29/month)"
|
||||
msgstr "Ulepsz do planu Pro (29 USD/miesiąc)"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Przesyłanie nie powiodło się"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2325,7 +2374,7 @@ msgstr "Zobacz i zarządzaj swoimi płatnościami i subskrypcją."
|
||||
msgid "View docs"
|
||||
msgstr "Zobacz dokumentację"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "Tylko podgląd"
|
||||
|
||||
@@ -2333,11 +2382,11 @@ msgstr "Tylko podgląd"
|
||||
msgid "View our roadmap."
|
||||
msgstr "Zobacz nasz plan ulepszeń."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "Zobacz obszar roboczy"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Widoczność"
|
||||
|
||||
@@ -2345,6 +2394,10 @@ msgstr "Widoczność"
|
||||
msgid "We are using the <0>AGPL-3.0 license</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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "Dopiero zaczynamy. "
|
||||
@@ -2374,10 +2427,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "Dlaczego próbujecie stworzyć Trello w wersji otwartoźródłowej?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Obszar roboczy"
|
||||
@@ -2483,6 +2536,10 @@ msgstr "Zostałeś pomyślnie zalogowany."
|
||||
msgid "You have been signed up successfully."
|
||||
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
|
||||
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!"
|
||||
@@ -2507,6 +2564,10 @@ msgstr "Twoje tablice zostały zaimportowane."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Twoja nazwa wyświetlana została zaktualizowana."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Twój plik został pomyślnie przesłany."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "Twoje hasło zostało zmienione."
|
||||
@@ -2523,6 +2584,10 @@ msgstr "Twoje konto Trello zostało odłączone."
|
||||
msgid "Your Trello account is connected."
|
||||
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
|
||||
msgid "Your workspace description has been updated."
|
||||
msgstr "Opis twojej przestrzeni roboczej został zaktualizowany."
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,17 +31,17 @@ msgstr "{0}"
|
||||
#. placeholder {1}: board?.name ?? t`Board`
|
||||
#. placeholder {1}: workspace.name ?? t`Workspace`
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "{0} | {1}"
|
||||
msgstr "{0} | {1}"
|
||||
|
||||
#. placeholder {0}: labelPublicIds.length
|
||||
#: src/views/board/components/NewCardForm.tsx:442
|
||||
#: src/views/board/components/NewCardForm.tsx:443
|
||||
msgid "{0} labels"
|
||||
msgstr "{0} метки"
|
||||
|
||||
#. placeholder {0}: isTemplate ? "Template" : "Board"
|
||||
#: src/views/board/index.tsx:387
|
||||
#: src/views/board/index.tsx:389
|
||||
msgid "{0} not found"
|
||||
msgstr "{0} не найден"
|
||||
|
||||
@@ -79,8 +79,8 @@ msgstr "Аккаунт"
|
||||
msgid "Account deleted"
|
||||
msgstr "Аккаунт удалён"
|
||||
|
||||
#: src/views/card/index.tsx:291
|
||||
#: src/views/public/board/CardModal.tsx:143
|
||||
#: src/views/card/index.tsx:343
|
||||
#: src/views/public/board/CardModal.tsx:157
|
||||
msgid "Activity"
|
||||
msgstr "Активность"
|
||||
|
||||
@@ -109,7 +109,7 @@ msgstr "Добавить элемент..."
|
||||
msgid "Add checklist"
|
||||
msgstr "Добавить чек-лист"
|
||||
|
||||
#: src/components/Editor.tsx:442
|
||||
#: src/components/Editor.tsx:451
|
||||
msgid "Add description... (type '/' to open commands or '@' to mention)"
|
||||
msgstr "Добавить описание... (введите '/' для открытия команд или '@' для упоминания)"
|
||||
|
||||
@@ -270,6 +270,10 @@ msgstr "назначено <0>{0}</0> к карточке"
|
||||
msgid "Assignees"
|
||||
msgstr "Исполнители"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:25
|
||||
msgid "Attachment uploaded"
|
||||
msgstr "Вложение загружено"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:47
|
||||
msgid "Awaiting Customer"
|
||||
msgstr "Ожидание клиента"
|
||||
@@ -304,9 +308,9 @@ msgstr "Портал выставления счетов"
|
||||
msgid "Blog Post"
|
||||
msgstr "Пост в блоге"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/card/index.tsx:229
|
||||
#: src/views/public/board/index.tsx:101
|
||||
msgid "Board"
|
||||
msgstr "Доска"
|
||||
|
||||
@@ -323,7 +327,7 @@ msgstr "Название доски не может превышать 100 си
|
||||
msgid "Board name is required"
|
||||
msgstr "Название доски обязательно"
|
||||
|
||||
#: src/views/public/board/index.tsx:151
|
||||
#: src/views/public/board/index.tsx:153
|
||||
msgid "Board not found"
|
||||
msgstr "Доска не найдена"
|
||||
|
||||
@@ -331,19 +335,19 @@ msgstr "Доска не найдена"
|
||||
msgid "Board sharing & invites"
|
||||
msgstr "Общий доступ к доске и приглашения"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:45
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:46
|
||||
msgid "Board URL can only contain letters, numbers, and hyphens"
|
||||
msgstr "URL доски может содержать только буквы, цифры и дефисы"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:43
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:44
|
||||
msgid "Board URL cannot exceed 60 characters"
|
||||
msgstr "URL доски не может превышать 60 символов"
|
||||
|
||||
#: src/views/public/board/index.tsx:67
|
||||
#: src/views/public/board/index.tsx:68
|
||||
msgid "Board URL copied to clipboard"
|
||||
msgstr "URL доски скопирован в буфер обмена"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:42
|
||||
msgid "Board URL must be at least 3 characters long"
|
||||
msgstr "URL доски должен содержать не менее 3 символов"
|
||||
|
||||
@@ -352,7 +356,7 @@ msgstr "URL доски должен содержать не менее 3 сим
|
||||
msgid "Board visibility"
|
||||
msgstr "Видимость доски"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:45
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
msgid "Board visibility updated"
|
||||
msgstr "Видимость доски обновлена"
|
||||
|
||||
@@ -387,15 +391,16 @@ msgstr "Отчет об ошибке"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/views/card/index.tsx:216
|
||||
#: src/views/card/index.tsx:229
|
||||
msgid "Card"
|
||||
msgstr "Карточка"
|
||||
|
||||
#: src/views/card/index.tsx:262
|
||||
#: src/views/card/index.tsx:263
|
||||
#: src/views/card/index.tsx:299
|
||||
msgid "Card not found"
|
||||
msgstr "Карточка не найдена"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:309
|
||||
#: src/views/board/components/NewCardForm.tsx:310
|
||||
msgid "Card title"
|
||||
msgstr "Название карточки"
|
||||
|
||||
@@ -428,7 +433,7 @@ msgstr "Название контрольного списка"
|
||||
msgid "Checklists"
|
||||
msgstr "Контрольные списки"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:155
|
||||
#: src/views/board/components/Filters.tsx:180
|
||||
msgid "Clear filters"
|
||||
msgstr "Очистить фильтры"
|
||||
|
||||
@@ -489,6 +494,10 @@ msgstr "завершил элемент контрольного списка"
|
||||
msgid "completed checklist item <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
|
||||
msgid "Confirm your new password"
|
||||
msgstr "Подтвердите ваш новый пароль"
|
||||
@@ -519,12 +528,12 @@ msgstr "Свяжитесь с нами"
|
||||
msgid "Content Creation"
|
||||
msgstr "Создание контента"
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Continue with "
|
||||
msgstr "Продолжить с "
|
||||
|
||||
#. placeholder {0}: key === "oidc" ? oidcProviderName : provider.name
|
||||
#: src/components/AuthForm.tsx:351
|
||||
#: src/components/AuthForm.tsx:355
|
||||
msgid "Continue with {0}"
|
||||
msgstr "Продолжить с {0}"
|
||||
|
||||
@@ -542,8 +551,8 @@ msgid "Create {0}"
|
||||
msgstr "Создать {0}"
|
||||
|
||||
#: src/components/LabelForm.tsx:213
|
||||
#: src/views/board/components/NewCardForm.tsx:468
|
||||
#: src/views/board/components/NewListForm.tsx:140
|
||||
#: src/views/board/components/NewCardForm.tsx:469
|
||||
#: src/views/board/components/NewListForm.tsx:141
|
||||
msgid "Create another"
|
||||
msgstr "Создать ещё одну"
|
||||
|
||||
@@ -551,7 +560,7 @@ msgstr "Создать ещё одну"
|
||||
msgid "Create API key"
|
||||
msgstr "Создать API-ключ"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:478
|
||||
#: src/views/board/components/NewCardForm.tsx:479
|
||||
msgid "Create card"
|
||||
msgstr "Создать карточку"
|
||||
|
||||
@@ -567,7 +576,7 @@ msgstr "Создать ссылку-приглашение"
|
||||
msgid "Create label"
|
||||
msgstr "Создать метку"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:152
|
||||
#: src/views/board/components/NewListForm.tsx:153
|
||||
msgid "Create list"
|
||||
msgstr "Создать список"
|
||||
|
||||
@@ -580,12 +589,12 @@ msgstr "Создать новый {0}"
|
||||
msgid "Create new key"
|
||||
msgstr "Создать новый ключ"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:404
|
||||
#: src/views/board/components/NewCardForm.tsx:405
|
||||
#: src/views/card/components/LabelSelector.tsx:97
|
||||
msgid "Create new label"
|
||||
msgstr "Создать новую метку"
|
||||
|
||||
#: src/views/board/index.tsx:475
|
||||
#: src/views/board/index.tsx:478
|
||||
msgid "Create new list"
|
||||
msgstr "Создать новый список"
|
||||
|
||||
@@ -743,6 +752,10 @@ msgstr "Отображаемое имя обновлено"
|
||||
msgid "Do you offer a free plan?"
|
||||
msgstr "Вы предлагаете бесплатный план?"
|
||||
|
||||
#: src/pages/unsubscribe/index.tsx:78
|
||||
msgid "Do you want to unsubscribe?"
|
||||
msgstr "Вы хотите отписаться?"
|
||||
|
||||
#: src/components/FeedbackModal.tsx:109
|
||||
msgid "docs"
|
||||
msgstr "документы"
|
||||
@@ -769,8 +782,12 @@ msgstr "Нет аккаунта? <0><1>Зарегистрируйтесь</1></0
|
||||
msgid "Done"
|
||||
msgstr "Готово"
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:141
|
||||
msgid "Download failed"
|
||||
msgstr "Не удалось загрузить"
|
||||
|
||||
#: src/views/board/components/BoardDropdown.tsx:41
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:115
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
|
||||
msgid "Edit board URL"
|
||||
msgstr "Изменить URL доски"
|
||||
|
||||
@@ -782,7 +799,7 @@ msgstr "Редактировать комментарий"
|
||||
msgid "Edit label"
|
||||
msgstr "Редактировать метку"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:161
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:162
|
||||
msgid "Edit workspace URL"
|
||||
msgstr "Редактировать URL рабочей области"
|
||||
|
||||
@@ -790,7 +807,7 @@ msgstr "Редактировать URL рабочей области"
|
||||
msgid "Editing"
|
||||
msgstr "Редактирование"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "email"
|
||||
msgstr "email"
|
||||
|
||||
@@ -810,11 +827,11 @@ msgstr "Введите ваш текущий пароль"
|
||||
msgid "Enter your current password and choose a new secure password."
|
||||
msgstr "Введите ваш текущий пароль и выберите новый безопасный пароль."
|
||||
|
||||
#: src/components/AuthForm.tsx:387
|
||||
#: src/components/AuthForm.tsx:402
|
||||
msgid "Enter your email address"
|
||||
msgstr "Введите ваш адрес электронной почты"
|
||||
|
||||
#: src/components/AuthForm.tsx:375
|
||||
#: src/components/AuthForm.tsx:388
|
||||
msgid "Enter your name"
|
||||
msgstr "Введите ваше имя"
|
||||
|
||||
@@ -822,7 +839,7 @@ msgstr "Введите ваше имя"
|
||||
msgid "Enter your new password"
|
||||
msgstr "Введите ваш новый пароль"
|
||||
|
||||
#: src/components/AuthForm.tsx:400
|
||||
#: src/components/AuthForm.tsx:416
|
||||
msgid "Enter your password"
|
||||
msgstr "Введите ваш пароль"
|
||||
|
||||
@@ -931,6 +948,11 @@ msgstr "Не удалось создать доску"
|
||||
msgid "Failed to login with {0}. Please try again."
|
||||
msgstr "Не удалось войти с {0}. Пожалуйста, попробуйте снова."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:33
|
||||
#: src/views/card/components/AttachmentUpload.tsx:79
|
||||
msgid "Failed to upload attachment. Please try again."
|
||||
msgstr "Не удалось загрузить вложение. Пожалуйста, попробуйте еще раз."
|
||||
|
||||
#: src/views/home/components/Header.tsx:41
|
||||
msgid "FAQ"
|
||||
msgstr "FAQ"
|
||||
@@ -974,7 +996,7 @@ msgstr "Обратная связь"
|
||||
msgid "Feedback sent"
|
||||
msgstr "Обратная связь отправлена"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:149
|
||||
#: src/views/board/components/Filters.tsx:174
|
||||
msgid "Filter"
|
||||
msgstr "Фильтр"
|
||||
|
||||
@@ -1028,7 +1050,7 @@ msgstr "Начать"
|
||||
msgid "Get started by creating a new {0}"
|
||||
msgstr "Начните с создания нового {0}"
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
#: src/views/board/index.tsx:470
|
||||
msgid "Get started by creating a new list"
|
||||
msgstr "Начните с создания нового списка"
|
||||
|
||||
@@ -1242,9 +1264,9 @@ msgstr "Канбан переосмыслен"
|
||||
msgid "Keep in mind that this action is irreversible."
|
||||
msgstr "Имейте в виду, что это действие необратимо."
|
||||
|
||||
#: src/views/board/components/Filters.tsx:101
|
||||
#: src/views/board/components/NewCardForm.tsx:408
|
||||
#: src/views/card/index.tsx:117
|
||||
#: src/views/board/components/Filters.tsx:115
|
||||
#: src/views/board/components/NewCardForm.tsx:409
|
||||
#: src/views/card/index.tsx:119
|
||||
msgid "Labels"
|
||||
msgstr "Метки"
|
||||
|
||||
@@ -1294,18 +1316,22 @@ msgstr "Лицензия"
|
||||
msgid "Light"
|
||||
msgstr "Светлый"
|
||||
|
||||
#: src/views/public/board/index.tsx:65
|
||||
#: src/views/public/board/index.tsx:66
|
||||
msgid "Link copied"
|
||||
msgstr "Ссылка скопирована"
|
||||
|
||||
#: src/views/card/index.tsx:109
|
||||
#: src/views/card/index.tsx:111
|
||||
msgid "List"
|
||||
msgstr "Список"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:128
|
||||
#: src/views/board/components/NewListForm.tsx:129
|
||||
msgid "List name"
|
||||
msgstr "Название списка"
|
||||
|
||||
#: src/views/board/components/Filters.tsx:123
|
||||
msgid "Lists"
|
||||
msgstr "Списки"
|
||||
|
||||
#: src/views/auth/login/index.tsx:33
|
||||
msgid "Login | kan.bn"
|
||||
msgstr "Вход | kan.bn"
|
||||
@@ -1326,7 +1352,7 @@ msgstr "Любим командами по всему миру"
|
||||
msgid "Low Priority"
|
||||
msgstr "Низкий приоритет"
|
||||
|
||||
#: src/components/AuthForm.tsx:421
|
||||
#: src/components/AuthForm.tsx:441
|
||||
msgid "magic link"
|
||||
msgstr "магическая ссылка"
|
||||
|
||||
@@ -1348,9 +1374,9 @@ msgid "Medium Priority"
|
||||
msgstr "Средний приоритет"
|
||||
|
||||
#: src/components/SideNavigation.tsx:101
|
||||
#: src/views/board/components/Filters.tsx:93
|
||||
#: src/views/board/components/NewCardForm.tsx:366
|
||||
#: src/views/card/index.tsx:126
|
||||
#: src/views/board/components/Filters.tsx:107
|
||||
#: src/views/board/components/NewCardForm.tsx:367
|
||||
#: src/views/card/index.tsx:128
|
||||
#: src/views/members/index.tsx:178
|
||||
msgid "Members"
|
||||
msgstr "Участники"
|
||||
@@ -1403,7 +1429,7 @@ msgstr "Новый {0}"
|
||||
msgid "New API key"
|
||||
msgstr "Новый API-ключ"
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:292
|
||||
#: src/views/board/components/NewCardForm.tsx:293
|
||||
msgid "New card"
|
||||
msgstr "Новая карточка"
|
||||
|
||||
@@ -1419,8 +1445,8 @@ msgstr "Новый импорт"
|
||||
msgid "New label"
|
||||
msgstr "Новая метка"
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:112
|
||||
#: src/views/board/index.tsx:439
|
||||
#: src/views/board/components/NewListForm.tsx:113
|
||||
#: src/views/board/index.tsx:442
|
||||
msgid "New list"
|
||||
msgstr "Новый список"
|
||||
|
||||
@@ -1457,6 +1483,10 @@ msgstr "Следующие шаги"
|
||||
msgid "No {0}"
|
||||
msgstr "Нет {0}"
|
||||
|
||||
#: src/components/AuthForm.tsx:369
|
||||
msgid "No authentication methods are currently available"
|
||||
msgstr "В настоящее время нет доступных методов аутентификации"
|
||||
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:242
|
||||
msgid "No boards found"
|
||||
msgstr "Доски не найдены"
|
||||
@@ -1465,7 +1495,11 @@ msgstr "Доски не найдены"
|
||||
msgid "No credit card required"
|
||||
msgstr "Кредитная карта не требуется"
|
||||
|
||||
#: src/views/board/index.tsx:464
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:142
|
||||
msgid "No download URL available for this attachment."
|
||||
msgstr "URL для загрузки этого вложения недоступен."
|
||||
|
||||
#: src/views/board/index.tsx:467
|
||||
msgid "No lists"
|
||||
msgstr "Нет списков"
|
||||
|
||||
@@ -1497,7 +1531,7 @@ msgstr "С открытым исходным кодом"
|
||||
msgid "Open Source Friends"
|
||||
msgstr "Друзья с открытым исходным кодом"
|
||||
|
||||
#: src/components/AuthForm.tsx:365
|
||||
#: src/components/AuthForm.tsx:378
|
||||
msgid "or"
|
||||
msgstr "или"
|
||||
|
||||
@@ -1579,15 +1613,15 @@ msgstr "Платформа"
|
||||
msgid "Please confirm your new password"
|
||||
msgstr "Пожалуйста, подтвердите новый пароль"
|
||||
|
||||
#: src/components/AuthForm.tsx:391
|
||||
#: src/components/AuthForm.tsx:406
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Пожалуйста, введите действительный адрес электронной почты"
|
||||
|
||||
#: src/components/AuthForm.tsx:379
|
||||
#: src/components/AuthForm.tsx:392
|
||||
msgid "Please enter a valid name"
|
||||
msgstr "Пожалуйста, введите действительное имя"
|
||||
|
||||
#: src/components/AuthForm.tsx:404
|
||||
#: src/components/AuthForm.tsx:421
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "Пожалуйста, введите действительный пароль"
|
||||
|
||||
@@ -1598,15 +1632,16 @@ msgstr "Пожалуйста, выберите файл для загрузки.
|
||||
#: src/components/DeleteLabelConfirmation.tsx:26
|
||||
#: src/components/FeedbackModal.tsx:41
|
||||
#: src/components/NewWorkspaceForm.tsx:158
|
||||
#: src/views/board/components/NewCardForm.tsx:176
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
#: src/views/board/components/NewCardForm.tsx:177
|
||||
#: src/views/board/components/NewListForm.tsx:79
|
||||
#: src/views/board/components/NewTemplateForm.tsx:61
|
||||
#: src/views/board/components/NewTemplateForm.tsx:77
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
#: src/views/board/index.tsx:149
|
||||
#: src/views/board/index.tsx:205
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:54
|
||||
#: src/views/board/index.tsx:151
|
||||
#: src/views/board/index.tsx:207
|
||||
#: src/views/boards/components/ImportBoardsForm.tsx:205
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:73
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:60
|
||||
#: src/views/card/components/ChecklistItemRow.tsx:88
|
||||
#: src/views/card/components/ChecklistNameInput.tsx:46
|
||||
@@ -1620,7 +1655,7 @@ msgstr "Пожалуйста, выберите файл для загрузки.
|
||||
#: src/views/card/components/NewChecklistForm.tsx:70
|
||||
#: src/views/card/components/NewChecklistItemForm.tsx:89
|
||||
#: src/views/card/components/NewCommentForm.tsx:31
|
||||
#: src/views/card/index.tsx:175
|
||||
#: src/views/card/index.tsx:177
|
||||
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
|
||||
#: src/views/members/components/InviteMemberForm.tsx:104
|
||||
#: src/views/members/components/InviteMemberForm.tsx:241
|
||||
@@ -1664,7 +1699,7 @@ msgstr "Приоритетная поддержка по электронной
|
||||
msgid "Privacy policy"
|
||||
msgstr "Политика конфиденциальности"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:73
|
||||
#: src/views/board/components/VisibilityButton.tsx:74
|
||||
msgid "Private"
|
||||
msgstr "Личное"
|
||||
|
||||
@@ -1684,7 +1719,7 @@ msgstr "Изображение профиля обновлено"
|
||||
msgid "Profile picture"
|
||||
msgstr "Фотография профиля"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:68
|
||||
#: src/views/board/components/VisibilityButton.tsx:69
|
||||
msgid "Public"
|
||||
msgstr "Публичное"
|
||||
|
||||
@@ -1886,7 +1921,7 @@ msgstr "Регистрация отключена"
|
||||
msgid "Sign up is currently disabled. Please try again later."
|
||||
msgstr "Регистрация в настоящее время отключена. Пожалуйста, попробуйте позже."
|
||||
|
||||
#: src/components/AuthForm.tsx:420
|
||||
#: src/components/AuthForm.tsx:440
|
||||
msgid "Sign up with "
|
||||
msgstr "Зарегистрироваться с "
|
||||
|
||||
@@ -1953,8 +1988,8 @@ msgstr "Командный план"
|
||||
msgid "Teams"
|
||||
msgstr "Команды"
|
||||
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:396
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/board/index.tsx:398
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
@@ -2004,7 +2039,7 @@ msgid "The open source <0/> alternative to Trello"
|
||||
msgstr "Альтернатива Trello с открытым исходным кодом <0/>"
|
||||
|
||||
#. placeholder {0}: isPublic ? "public" : "private"
|
||||
#: src/views/board/components/VisibilityButton.tsx:46
|
||||
#: src/views/board/components/VisibilityButton.tsx:47
|
||||
msgid "The visibility of your board has been set to {0}."
|
||||
msgstr "Видимость вашей доски установлена на {0}."
|
||||
|
||||
@@ -2028,11 +2063,11 @@ msgstr "Это действие нельзя будет отменить."
|
||||
msgid "This API key will only be shown once. Please save it in a secure location."
|
||||
msgstr "Этот API-ключ будет показан только один раз. Пожалуйста, сохраните его в безопасном месте."
|
||||
|
||||
#: src/views/public/board/index.tsx:154
|
||||
#: src/views/public/board/index.tsx:156
|
||||
msgid "This board is private or does not exist"
|
||||
msgstr "Эта доска является приватной или не существует."
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:135
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:136
|
||||
msgid "This board URL has already been taken"
|
||||
msgstr "Этот URL доски уже занят."
|
||||
|
||||
@@ -2102,7 +2137,7 @@ msgstr "Не удалось добавить элемент контрольно
|
||||
msgid "Unable to add comment"
|
||||
msgstr "Не удалось добавить комментарий."
|
||||
|
||||
#: src/views/board/components/NewCardForm.tsx:173
|
||||
#: src/views/board/components/NewCardForm.tsx:174
|
||||
msgid "Unable to create card"
|
||||
msgstr "Не удалось создать карточку."
|
||||
|
||||
@@ -2110,7 +2145,7 @@ msgstr "Не удалось создать карточку."
|
||||
msgid "Unable to create checklist"
|
||||
msgstr "Не удалось создать контрольный список."
|
||||
|
||||
#: src/views/board/components/NewListForm.tsx:77
|
||||
#: src/views/board/components/NewListForm.tsx:78
|
||||
msgid "Unable to create list"
|
||||
msgstr "Не удалось создать список."
|
||||
|
||||
@@ -2123,6 +2158,10 @@ msgstr "Не удалось создать шаблон"
|
||||
msgid "Unable to create workspace"
|
||||
msgstr "Не удалось создать рабочее пространство."
|
||||
|
||||
#: src/views/card/components/AttachmentThumbnails.tsx:72
|
||||
msgid "Unable to delete attachment"
|
||||
msgstr "Не удалось удалить вложение"
|
||||
|
||||
#: src/views/card/components/DeleteCardConfirmation.tsx:51
|
||||
msgid "Unable to delete card"
|
||||
msgstr "Не удалось удалить карточку."
|
||||
@@ -2147,16 +2186,16 @@ msgstr "Не удалось удалить участника"
|
||||
msgid "Unable to send feedback"
|
||||
msgstr "Не удалось отправить отзыв"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:71
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
|
||||
msgid "Unable to update board URL"
|
||||
msgstr "Не удалось обновить URL доски"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:52
|
||||
#: src/views/board/components/VisibilityButton.tsx:53
|
||||
msgid "Unable to update board visibility"
|
||||
msgstr "Не удалось обновить видимость доски"
|
||||
|
||||
#: src/views/board/index.tsx:204
|
||||
#: src/views/card/index.tsx:174
|
||||
#: src/views/board/index.tsx:206
|
||||
#: src/views/card/index.tsx:176
|
||||
msgid "Unable to update card"
|
||||
msgstr "Не удалось обновить карточку"
|
||||
|
||||
@@ -2176,7 +2215,7 @@ msgstr "Не удалось обновить комментарий"
|
||||
msgid "Unable to update labels"
|
||||
msgstr "Не удалось обновить метки"
|
||||
|
||||
#: src/views/board/index.tsx:148
|
||||
#: src/views/board/index.tsx:150
|
||||
#: src/views/card/components/ListSelector.tsx:51
|
||||
msgid "Unable to update list"
|
||||
msgstr "Не удалось обновить список"
|
||||
@@ -2232,7 +2271,12 @@ msgstr "Неограниченное количество участников"
|
||||
msgid "Unlimited workspaces"
|
||||
msgstr "Неограниченное количество рабочих пространств"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
|
||||
#: src/pages/unsubscribe/index.tsx:68
|
||||
#: src/pages/unsubscribe/index.tsx:93
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Отписаться"
|
||||
|
||||
#: src/views/board/components/UpdateBoardSlugForm.tsx:175
|
||||
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
|
||||
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
|
||||
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
|
||||
@@ -2271,6 +2315,11 @@ msgstr "Обновить до Pro"
|
||||
msgid "Upgrade to Pro ($29/month)"
|
||||
msgstr "Обновить до Pro ($29/месяц)"
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:32
|
||||
#: src/views/card/components/AttachmentUpload.tsx:78
|
||||
msgid "Upload failed"
|
||||
msgstr "Не удалось загрузить"
|
||||
|
||||
#: src/views/boards/components/TemplateBoards.tsx:38
|
||||
#: src/views/boards/components/TemplateBoards.tsx:55
|
||||
msgid "Urgent"
|
||||
@@ -2320,7 +2369,7 @@ msgstr "Просмотр и управление вашими счетами и
|
||||
msgid "View docs"
|
||||
msgstr "Просмотреть документацию"
|
||||
|
||||
#: src/views/public/board/index.tsx:128
|
||||
#: src/views/public/board/index.tsx:129
|
||||
msgid "View only"
|
||||
msgstr "Только просмотр"
|
||||
|
||||
@@ -2328,11 +2377,11 @@ msgstr "Только просмотр"
|
||||
msgid "View our roadmap."
|
||||
msgstr "Просмотреть нашу дорожную карту."
|
||||
|
||||
#: src/views/public/board/index.tsx:157
|
||||
#: src/views/public/board/index.tsx:159
|
||||
msgid "View workspace"
|
||||
msgstr "Просмотреть рабочее пространство"
|
||||
|
||||
#: src/views/board/components/VisibilityButton.tsx:91
|
||||
#: src/views/board/components/VisibilityButton.tsx:92
|
||||
msgid "Visibility"
|
||||
msgstr "Видимость"
|
||||
|
||||
@@ -2340,6 +2389,10 @@ msgstr "Видимость"
|
||||
msgid "We are using the <0>AGPL-3.0 license</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
|
||||
msgid "We're just getting started. "
|
||||
msgstr "Мы только начинаем. "
|
||||
@@ -2369,10 +2422,10 @@ msgid "Why make an open source Trello?"
|
||||
msgstr "Зачем создавать Trello с открытым исходным кодом?"
|
||||
|
||||
#: src/components/SettingsLayout.tsx:39
|
||||
#: src/views/board/index.tsx:361
|
||||
#: src/views/board/index.tsx:363
|
||||
#: src/views/boards/index.tsx:22
|
||||
#: src/views/members/index.tsx:173
|
||||
#: src/views/public/board/index.tsx:100
|
||||
#: src/views/public/board/index.tsx:101
|
||||
#: src/views/public/boards/index.tsx:66
|
||||
msgid "Workspace"
|
||||
msgstr "Рабочее пространство"
|
||||
@@ -2478,6 +2531,10 @@ msgstr "Вы успешно вошли в систему."
|
||||
msgid "You have been signed up successfully."
|
||||
msgstr "Вы успешно зарегистрировались."
|
||||
|
||||
#: src/pages/unsubscribe/index.tsx:98
|
||||
msgid "You have been unsubscribed!"
|
||||
msgstr "Вы успешно отписались!"
|
||||
|
||||
#: src/views/members/components/InviteMemberForm.tsx:326
|
||||
msgid "You have unlimited seats with your Pro Plan. There is no additional charge for new members!"
|
||||
msgstr "В вашем профессиональном плане неограниченное количество мест. За новых участников дополнительная плата не взимается!"
|
||||
@@ -2502,6 +2559,10 @@ msgstr "Ваши доски были импортированы."
|
||||
msgid "Your display name has been updated."
|
||||
msgstr "Ваше отображаемое имя было обновлено."
|
||||
|
||||
#: src/views/card/components/AttachmentUpload.tsx:26
|
||||
msgid "Your file has been uploaded successfully."
|
||||
msgstr "Ваш файл успешно загружен."
|
||||
|
||||
#: src/views/settings/components/ChangePasswordConfirmation.tsx:70
|
||||
msgid "Your password has been changed."
|
||||
msgstr "Ваш пароль был изменён."
|
||||
@@ -2518,6 +2579,10 @@ msgstr "Ваш аккаунт Trello был отключён."
|
||||
msgid "Your Trello account is connected."
|
||||
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
|
||||
msgid "Your workspace description has been updated."
|
||||
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 { useEffect } from "react";
|
||||
|
||||
import { KeyboardShortcutProvider } from "~/providers/keyboard-shortcuts";
|
||||
import { LinguiProviderWrapper } from "~/providers/lingui";
|
||||
import { ModalProvider } from "~/providers/modal";
|
||||
import { PopupProvider } from "~/providers/popup";
|
||||
@@ -80,21 +81,23 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
|
||||
)}
|
||||
<script src="/__ENV.js" />
|
||||
<main className="font-sans">
|
||||
<LinguiProviderWrapper>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<ModalProvider>
|
||||
<PopupProvider>
|
||||
{posthogKey ? (
|
||||
<PostHogProvider client={posthog}>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</PostHogProvider>
|
||||
) : (
|
||||
getLayout(<Component {...pageProps} />)
|
||||
)}
|
||||
</PopupProvider>
|
||||
</ModalProvider>
|
||||
</ThemeProvider>
|
||||
</LinguiProviderWrapper>
|
||||
<KeyboardShortcutProvider>
|
||||
<LinguiProviderWrapper>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<ModalProvider>
|
||||
<PopupProvider>
|
||||
{posthogKey ? (
|
||||
<PostHogProvider client={posthog}>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</PostHogProvider>
|
||||
) : (
|
||||
getLayout(<Component {...pageProps} />)
|
||||
)}
|
||||
</PopupProvider>
|
||||
</ModalProvider>
|
||||
</ThemeProvider>
|
||||
</LinguiProviderWrapper>
|
||||
</KeyboardShortcutProvider>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
47
apps/web/src/pages/api/download/attatchment.ts
Normal file
47
apps/web/src/pages/api/download/attatchment.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { url, filename } = req.query;
|
||||
|
||||
if (!url || typeof url !== "string") {
|
||||
return res.status(400).json({
|
||||
message: "url parameter is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const downloadFilename = typeof filename === "string"
|
||||
? encodeURIComponent(filename)
|
||||
: "attachment";
|
||||
|
||||
const upstream = await fetch(url);
|
||||
|
||||
if (!upstream.ok) {
|
||||
return res.status(upstream.status).json({
|
||||
message: "Failed to fetch attachment",
|
||||
});
|
||||
}
|
||||
|
||||
const contentType =
|
||||
upstream.headers.get("Content-Type") ?? "application/octet-stream";
|
||||
|
||||
res.setHeader("Content-Type", contentType);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${downloadFilename}"; filename*=UTF-8''${downloadFilename}`,
|
||||
);
|
||||
|
||||
const buffer = await upstream.arrayBuffer();
|
||||
return res.send(Buffer.from(buffer));
|
||||
} catch (error) {
|
||||
console.error("Error downloading attachment:", error);
|
||||
return res.status(500).json({ message: "Failed to download attachment" });
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HiOutlinePaperClip } from "react-icons/hi";
|
||||
import { HiBars3BottomLeft, HiChatBubbleLeft } from "react-icons/hi2";
|
||||
|
||||
import Avatar from "~/components/Avatar";
|
||||
@@ -13,6 +14,7 @@ const Card = ({
|
||||
checklists,
|
||||
description,
|
||||
comments,
|
||||
attachments,
|
||||
}: {
|
||||
title: string;
|
||||
labels: { name: string; colourCode: string | null }[];
|
||||
@@ -33,6 +35,7 @@ const Card = ({
|
||||
}[];
|
||||
description: string | null;
|
||||
comments: { publicId: string }[];
|
||||
attachments?: { publicId: string }[];
|
||||
}) => {
|
||||
const completedItems = checklists.reduce((acc, checklist) => {
|
||||
return acc + checklist.items.filter((item) => item.completed).length;
|
||||
@@ -47,6 +50,7 @@ const Card = ({
|
||||
|
||||
const hasDescription =
|
||||
description && description.replace(/<[^>]*>/g, "").trim().length > 0;
|
||||
const hasAttachments = attachments && attachments.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
|
||||
@@ -55,7 +59,8 @@ const Card = ({
|
||||
members.length ||
|
||||
checklists.length > 0 ||
|
||||
hasDescription ||
|
||||
comments.length > 0 ? (
|
||||
comments.length > 0 ||
|
||||
hasAttachments ? (
|
||||
<div className="mt-2 flex flex-col justify-end">
|
||||
<div className="space-x-0.5">
|
||||
{labels.map((label) => (
|
||||
@@ -77,6 +82,11 @@ const Card = ({
|
||||
<HiChatBubbleLeft className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
{hasAttachments && (
|
||||
<div className="flex items-center gap-1 text-light-700 dark:text-dark-800">
|
||||
<HiOutlinePaperClip className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{checklists.length > 0 && (
|
||||
|
||||
@@ -12,6 +12,7 @@ interface QueryParams {
|
||||
boardPublicId: string;
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
}
|
||||
|
||||
export function DeleteListConfirmation({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRouter } from "next/router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import {
|
||||
HiMiniXMark,
|
||||
HiOutlineSquare3Stack3D,
|
||||
HiOutlineTag,
|
||||
HiOutlineUserCircle,
|
||||
} from "react-icons/hi2";
|
||||
@@ -32,15 +33,22 @@ interface Label {
|
||||
colourCode: string | null;
|
||||
}
|
||||
|
||||
interface List {
|
||||
publicId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const Filters = ({
|
||||
position = "right",
|
||||
labels,
|
||||
members,
|
||||
lists,
|
||||
isLoading,
|
||||
}: {
|
||||
position?: "left" | "right";
|
||||
labels: Label[];
|
||||
members: Member[];
|
||||
lists: List[];
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@@ -52,7 +60,7 @@ const Filters = ({
|
||||
try {
|
||||
await router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, members: [], labels: [] },
|
||||
query: { ...router.query, members: [], labels: [], lists: [] },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -85,6 +93,12 @@ const Filters = ({
|
||||
leftIcon: <LabelIcon colourCode={label.colourCode} />,
|
||||
}));
|
||||
|
||||
const formattedLists = lists.map((list) => ({
|
||||
key: list.publicId,
|
||||
value: list.name,
|
||||
selected: !!router.query.lists?.includes(list.publicId),
|
||||
}));
|
||||
|
||||
const groups = [
|
||||
...(formattedMembers.length
|
||||
? [
|
||||
@@ -102,6 +116,16 @@ const Filters = ({
|
||||
icon: <HiOutlineTag size={16} />,
|
||||
items: formattedLabels,
|
||||
},
|
||||
...(formattedLists.length
|
||||
? [
|
||||
{
|
||||
key: "lists",
|
||||
label: t`Lists`,
|
||||
icon: <HiOutlineSquare3Stack3D size={16} />,
|
||||
items: formattedLists,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const handleSelect = async (
|
||||
@@ -131,6 +155,7 @@ const Filters = ({
|
||||
const numOfFilters = [
|
||||
...formatToArray(router.query.members),
|
||||
...formatToArray(router.query.labels),
|
||||
...formatToArray(router.query.lists),
|
||||
].length;
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,6 +33,7 @@ interface QueryParams {
|
||||
boardPublicId: string;
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
}
|
||||
|
||||
interface NewCardFormProps {
|
||||
|
||||
@@ -21,6 +21,7 @@ interface QueryParams {
|
||||
boardPublicId: string;
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
}
|
||||
|
||||
export function NewListForm({
|
||||
|
||||
@@ -17,6 +17,7 @@ interface QueryParams {
|
||||
boardPublicId: string;
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
}
|
||||
|
||||
export function UpdateBoardSlugForm({
|
||||
|
||||
@@ -11,6 +11,7 @@ interface QueryParams {
|
||||
boardPublicId: string;
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
}
|
||||
|
||||
const VisibilityButton = ({
|
||||
|
||||
@@ -23,6 +23,8 @@ import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import PatternedBackground from "~/components/PatternedBackground";
|
||||
import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppable";
|
||||
import { Tooltip } from "~/components/Tooltip";
|
||||
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
@@ -54,6 +56,15 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
useState<PublicListId>("");
|
||||
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
|
||||
? Array.isArray(params.boardId)
|
||||
? params.boardId[0]
|
||||
@@ -80,11 +91,13 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
boardPublicId: string;
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
type: "regular" | "template";
|
||||
} = {
|
||||
boardPublicId: boardId ?? "",
|
||||
members: formatToArray(router.query.members),
|
||||
labels: formatToArray(router.query.labels),
|
||||
lists: formatToArray(router.query.lists),
|
||||
type: isTemplate ? "template" : "regular",
|
||||
};
|
||||
|
||||
@@ -418,26 +431,29 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
members={boardData.workspace.members.filter(
|
||||
(member) => member.user !== null,
|
||||
)}
|
||||
lists={boardData.allLists}
|
||||
position="left"
|
||||
isLoading={!boardData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
iconLeft={
|
||||
<HiOutlinePlusSmall
|
||||
className="-mr-0.5 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
if (boardId) openNewListForm(boardId);
|
||||
}}
|
||||
disabled={!boardData}
|
||||
>
|
||||
{t`New list`}
|
||||
</Button>
|
||||
<Tooltip content={createListShortcutTooltipContent}>
|
||||
<Button
|
||||
iconLeft={
|
||||
<HiOutlinePlusSmall
|
||||
className="-mr-0.5 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
if (boardId) openNewListForm(boardId);
|
||||
}}
|
||||
disabled={!boardData}
|
||||
>
|
||||
{t`New list`}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<BoardDropdown
|
||||
isTemplate={!!isTemplate}
|
||||
isLoading={!boardData}
|
||||
@@ -550,6 +566,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
card.description ?? null
|
||||
}
|
||||
comments={card.comments ?? []}
|
||||
attachments={card.attachments}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,8 @@ import FeedbackModal from "~/components/FeedbackModal";
|
||||
import Modal from "~/components/modal";
|
||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import { Tooltip } from "~/components/Tooltip";
|
||||
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { BoardsList } from "./components/BoardsList";
|
||||
@@ -16,6 +18,15 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
const { openModal, modalContentType, isOpen } = useModal();
|
||||
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 (
|
||||
<>
|
||||
<PageHead
|
||||
@@ -39,16 +50,18 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
{t`Import`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={() => openModal("NEW_BOARD")}
|
||||
iconLeft={
|
||||
<HiOutlinePlusSmall aria-hidden="true" className="h-4 w-4" />
|
||||
}
|
||||
>
|
||||
{t`New`}
|
||||
</Button>
|
||||
<Tooltip content={createModalShortcutTooltipContent}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={() => openModal("NEW_BOARD")}
|
||||
iconLeft={
|
||||
<HiOutlinePlusSmall aria-hidden="true" className="h-4 w-4" />
|
||||
}
|
||||
>
|
||||
{t`New`}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
462
apps/web/src/views/card/components/AttachmentThumbnails.tsx
Normal file
462
apps/web/src/views/card/components/AttachmentThumbnails.tsx
Normal file
@@ -0,0 +1,462 @@
|
||||
import Image from "next/image";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import {
|
||||
HiArrowDownTray,
|
||||
HiChevronLeft,
|
||||
HiChevronRight,
|
||||
HiDocumentText,
|
||||
HiOutlineTrash,
|
||||
HiXMark,
|
||||
} from "react-icons/hi2";
|
||||
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
interface Attachment {
|
||||
publicId: string;
|
||||
contentType: string;
|
||||
url: string | null;
|
||||
originalFilename: string | null;
|
||||
s3Key: string;
|
||||
size?: number | null;
|
||||
}
|
||||
|
||||
export function AttachmentThumbnails({
|
||||
attachments,
|
||||
cardPublicId,
|
||||
isReadOnly = false,
|
||||
}: {
|
||||
attachments?: Attachment[];
|
||||
cardPublicId: string;
|
||||
isReadOnly?: boolean;
|
||||
}) {
|
||||
const { showPopup } = usePopup();
|
||||
const utils = api.useUtils();
|
||||
const imageAttachments =
|
||||
attachments?.filter(
|
||||
(attachment) =>
|
||||
attachment.contentType.startsWith("image/") && attachment.url,
|
||||
) ?? [];
|
||||
|
||||
const nonImageAttachments =
|
||||
attachments?.filter(
|
||||
(attachment) =>
|
||||
!attachment.contentType.startsWith("image/") && attachment.url,
|
||||
) ?? [];
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||
|
||||
const deleteAttachment = api.attachment.delete.useMutation({
|
||||
onMutate: async (args) => {
|
||||
if (isReadOnly) return;
|
||||
|
||||
await utils.card.byId.cancel({ cardPublicId });
|
||||
const currentState = utils.card.byId.getData({ cardPublicId });
|
||||
|
||||
utils.card.byId.setData({ cardPublicId }, (oldCard) => {
|
||||
if (!oldCard) return oldCard;
|
||||
const updatedAttachments = oldCard.attachments.filter(
|
||||
(attachment) => attachment.publicId !== args.attachmentPublicId,
|
||||
);
|
||||
return { ...oldCard, attachments: updatedAttachments };
|
||||
});
|
||||
|
||||
return { previousState: currentState };
|
||||
},
|
||||
onError: (_error, _args, context) => {
|
||||
if (isReadOnly) return;
|
||||
utils.card.byId.setData({ cardPublicId }, context?.previousState);
|
||||
showPopup({
|
||||
header: t`Unable to delete attachment`,
|
||||
message: t`Please try again later, or contact customer support.`,
|
||||
icon: "error",
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (isReadOnly) return;
|
||||
// Close viewer if the deleted image was being viewed
|
||||
setSelectedIndex(null);
|
||||
},
|
||||
onSettled: async () => {
|
||||
if (isReadOnly) return;
|
||||
await utils.card.byId.invalidate({ cardPublicId });
|
||||
},
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
if (selectedIndex === null) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setSelectedIndex(null);
|
||||
} else if (e.key === "ArrowLeft") {
|
||||
setSelectedIndex((prev) => {
|
||||
if (prev === null) return null;
|
||||
return prev === 0 ? imageAttachments.length - 1 : prev - 1;
|
||||
});
|
||||
} else if (e.key === "ArrowRight") {
|
||||
setSelectedIndex((prev) => {
|
||||
if (prev === null) return null;
|
||||
return prev === imageAttachments.length - 1 ? 0 : prev + 1;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedIndex, imageAttachments.length]);
|
||||
|
||||
if (imageAttachments.length === 0 && nonImageAttachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const openViewer = (index: number) => {
|
||||
setSelectedIndex(index);
|
||||
};
|
||||
|
||||
const closeViewer = () => {
|
||||
setSelectedIndex(null);
|
||||
};
|
||||
|
||||
const goToPrevious = () => {
|
||||
if (selectedIndex === null) return;
|
||||
const newIndex =
|
||||
selectedIndex === 0 ? imageAttachments.length - 1 : selectedIndex - 1;
|
||||
setSelectedIndex(newIndex);
|
||||
};
|
||||
|
||||
const goToNext = () => {
|
||||
if (selectedIndex === null) return;
|
||||
const newIndex =
|
||||
selectedIndex === imageAttachments.length - 1 ? 0 : selectedIndex + 1;
|
||||
setSelectedIndex(newIndex);
|
||||
};
|
||||
|
||||
const handleDownload = (attachment: Attachment) => {
|
||||
if (!attachment.url) {
|
||||
showPopup({
|
||||
header: t`Download failed`,
|
||||
message: t`No download URL available for this attachment.`,
|
||||
icon: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadUrl = `/api/download/attatchment?url=${encodeURIComponent(attachment.url)}&filename=${encodeURIComponent(attachment.originalFilename ?? "attachment")}`;
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
};
|
||||
|
||||
const selectedAttachment =
|
||||
selectedIndex !== null ? imageAttachments[selectedIndex] : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3 flex flex-wrap gap-2 pt-1">
|
||||
{imageAttachments.map((attachment, index) => {
|
||||
if (!attachment.url) return null;
|
||||
return (
|
||||
<AttachmentThumbnail
|
||||
key={attachment.publicId}
|
||||
attachment={{
|
||||
publicId: attachment.publicId,
|
||||
url: attachment.url,
|
||||
originalFilename: attachment.originalFilename ?? "",
|
||||
contentType: attachment.contentType,
|
||||
}}
|
||||
onClick={() => openViewer(index)}
|
||||
isImage={true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{nonImageAttachments.length > 0 && (
|
||||
<div className="mb-3 flex flex-col gap-2">
|
||||
{nonImageAttachments.map((attachment) => {
|
||||
if (!attachment.url) return null;
|
||||
return (
|
||||
<FileListItem
|
||||
key={attachment.publicId}
|
||||
attachment={attachment}
|
||||
onDownload={() => handleDownload(attachment)}
|
||||
onDelete={
|
||||
isReadOnly
|
||||
? undefined
|
||||
: () => {
|
||||
deleteAttachment.mutate({
|
||||
attachmentPublicId: attachment.publicId,
|
||||
});
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Transition.Root show={selectedIndex !== null} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50"
|
||||
onClose={() => {
|
||||
// Dialog closing is handled by the background overlay click
|
||||
}}
|
||||
static
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 bg-light-50 transition-opacity dark:bg-dark-50"
|
||||
onClick={(e) => {
|
||||
// Only close if clicking directly on the background, not on buttons
|
||||
if (e.target === e.currentTarget) {
|
||||
closeViewer();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
{selectedIndex !== null && selectedAttachment && (
|
||||
<div
|
||||
className="fixed left-2 top-2 z-20 flex gap-1"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{!isReadOnly && (
|
||||
<button
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteAttachment.mutate({
|
||||
attachmentPublicId: selectedAttachment.publicId,
|
||||
});
|
||||
}}
|
||||
className="rounded-full bg-light-50 p-1.5 text-light-1000 transition-colors hover:bg-light-100 focus:outline-none dark:bg-dark-50 dark:text-dark-1000 dark:hover:bg-dark-100"
|
||||
aria-label="Delete image"
|
||||
disabled={deleteAttachment.isPending}
|
||||
>
|
||||
<HiOutlineTrash className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDownload(selectedAttachment);
|
||||
}}
|
||||
className="rounded-full bg-light-50 p-1.5 text-light-1000 transition-colors hover:bg-light-100 focus:outline-none dark:bg-dark-50 dark:text-dark-1000 dark:hover:bg-dark-100"
|
||||
aria-label="Download image"
|
||||
>
|
||||
<HiArrowDownTray className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="fixed right-2 top-2 z-20 flex gap-1">
|
||||
{imageAttachments.length > 1 && selectedIndex !== null && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToPrevious();
|
||||
}}
|
||||
className="rounded-full bg-light-50 p-1.5 text-light-1000 transition-colors hover:bg-light-100 focus:outline-none dark:bg-dark-50 dark:text-dark-1000 dark:hover:bg-dark-100"
|
||||
aria-label="Previous image"
|
||||
>
|
||||
<HiChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{imageAttachments.length > 1 && selectedIndex !== null && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToNext();
|
||||
}}
|
||||
className="rounded-full bg-light-50 p-1.5 text-light-1000 transition-colors hover:bg-light-100 focus:outline-none dark:bg-dark-50 dark:text-dark-1000 dark:hover:bg-dark-100"
|
||||
aria-label="Next image"
|
||||
>
|
||||
<HiChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedIndex !== null && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
closeViewer();
|
||||
}}
|
||||
className="rounded-full bg-light-50 p-1.5 text-light-1000 transition-colors hover:bg-light-100 focus:outline-none dark:bg-dark-50 dark:text-dark-1000 dark:hover:bg-dark-100"
|
||||
aria-label="Close"
|
||||
>
|
||||
<HiXMark className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-full items-center justify-center p-4">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
className="relative w-full max-w-7xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{selectedAttachment?.url && (
|
||||
<div className="relative">
|
||||
<div className="relative mx-auto max-h-[90vh] w-full">
|
||||
<Image
|
||||
src={selectedAttachment.url}
|
||||
alt={
|
||||
selectedAttachment.originalFilename ?? "Attachment"
|
||||
}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="mx-auto max-h-[90vh] w-auto object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imageAttachments.length > 1 && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full bg-black/50 px-3 py-1.5 text-[10px] text-white">
|
||||
{selectedIndex !== null && selectedIndex + 1} /{" "}
|
||||
{imageAttachments.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentThumbnail({
|
||||
attachment,
|
||||
onClick,
|
||||
isImage,
|
||||
}: {
|
||||
attachment: {
|
||||
publicId: string;
|
||||
url: string;
|
||||
originalFilename: string;
|
||||
contentType: string;
|
||||
};
|
||||
onClick: () => void;
|
||||
isImage: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="relative h-16 w-16 overflow-hidden rounded-xl border border-light-300 transition-transform hover:scale-105 dark:border-dark-300"
|
||||
aria-label={`View ${attachment.originalFilename}`}
|
||||
>
|
||||
{isImage ? (
|
||||
<Image
|
||||
src={attachment.url}
|
||||
alt={attachment.originalFilename}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="64px"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-light-100 dark:bg-dark-100">
|
||||
<HiDocumentText className="h-6 w-6 text-light-700 dark:text-dark-700" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number | null | undefined): string {
|
||||
if (!bytes || bytes === 0 || isNaN(bytes)) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i] ?? "B"}`;
|
||||
}
|
||||
|
||||
function FileListItem({
|
||||
attachment,
|
||||
onDownload,
|
||||
onDelete,
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
onDownload: () => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="group flex w-full items-center gap-3 rounded-lg border border-light-300 bg-light-50 px-3 py-2 dark:border-dark-200 dark:bg-dark-100">
|
||||
<div className="flex-shrink-0">
|
||||
<HiDocumentText className="h-5 w-5 text-light-700 dark:text-dark-700" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 truncate text-sm text-light-1000 dark:text-dark-1000">
|
||||
{attachment.originalFilename ?? "File"}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="text-xs text-light-500 dark:text-dark-900">
|
||||
{attachment.size != null &&
|
||||
!isNaN(attachment.size) &&
|
||||
`${formatFileSize(attachment.size)}`}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDownload();
|
||||
}}
|
||||
className="flex-shrink-0 rounded-full bg-light-100 p-1.5 text-light-1000 transition-colors hover:bg-light-200 focus:outline-none dark:bg-dark-100 dark:text-dark-950 dark:hover:bg-dark-300"
|
||||
aria-label={`Download ${attachment.originalFilename}`}
|
||||
>
|
||||
<HiArrowDownTray className="h-4 w-4" />
|
||||
</button>
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="flex-shrink-0 rounded-full bg-light-100 p-1.5 text-light-1000 transition-colors hover:bg-light-200 focus:outline-none dark:bg-dark-100 dark:text-dark-950 dark:hover:bg-dark-300"
|
||||
aria-label={`Delete ${attachment.originalFilename}`}
|
||||
>
|
||||
<HiXMark className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
174
apps/web/src/views/card/components/AttachmentUpload.tsx
Normal file
174
apps/web/src/views/card/components/AttachmentUpload.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useRef, useState } from "react";
|
||||
import { HiOutlinePaperClip } from "react-icons/hi";
|
||||
import { HiCheckBadge } from "react-icons/hi2";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export function AttachmentUpload({ cardPublicId }: { cardPublicId: string }) {
|
||||
const { openModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const utils = api.useUtils();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const generateUploadUrl = api.attachment.generateUploadUrl.useMutation();
|
||||
const confirmAttachment = api.attachment.confirm.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.card.byId.invalidate({ cardPublicId });
|
||||
showPopup({
|
||||
header: t`Attachment uploaded`,
|
||||
message: t`Your file has been uploaded successfully.`,
|
||||
icon: "success",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showPopup({
|
||||
header: t`Upload failed`,
|
||||
message: t`Failed to upload attachment. Please try again.`,
|
||||
icon: "error",
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
setUploading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
// Generate presigned URL
|
||||
const { url, key } = await generateUploadUrl.mutateAsync({
|
||||
cardPublicId,
|
||||
filename: file.name,
|
||||
contentType: file.type,
|
||||
size: file.size,
|
||||
});
|
||||
|
||||
// Upload file to S3
|
||||
const uploadResponse = await fetch(url, {
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
},
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
|
||||
// Confirm attachment in database
|
||||
await confirmAttachment.mutateAsync({
|
||||
cardPublicId,
|
||||
s3Key: key,
|
||||
filename: file.name,
|
||||
originalFilename: file.name,
|
||||
contentType: file.type,
|
||||
size: file.size,
|
||||
});
|
||||
} catch {
|
||||
showPopup({
|
||||
header: t`Upload failed`,
|
||||
message: t`Failed to upload attachment. Please try again.`,
|
||||
icon: "error",
|
||||
});
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Reset input
|
||||
event.target.value = "";
|
||||
|
||||
await uploadFile(file);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!uploading) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = async (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
if (uploading) return;
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length === 0) return;
|
||||
|
||||
// Upload the first file (or could upload all files)
|
||||
await uploadFile(files[0] ?? new File([], ""));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
id="attachment-upload"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
disabled={uploading}
|
||||
/>
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={twMerge(
|
||||
"rounded-lg border-2 border-dashed transition-colors",
|
||||
isDragging
|
||||
? "border-light-300 bg-light-100 dark:border-dark-300 dark:bg-dark-100"
|
||||
: "border-transparent",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between p-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
iconLeft={
|
||||
<HiCheckBadge className="h-4 w-4 text-light-950 dark:text-dark-950" />
|
||||
}
|
||||
iconOnly
|
||||
size="sm"
|
||||
onClick={() => openModal("ADD_CHECKLIST")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
iconLeft={
|
||||
<HiOutlinePaperClip className="h-4 w-4 text-light-950 dark:text-dark-950" />
|
||||
}
|
||||
isLoading={uploading}
|
||||
disabled={uploading}
|
||||
iconOnly
|
||||
size="sm"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default function BoardDropdown() {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<HiEllipsisHorizontal className="h-6 w-6 text-dark-900" />
|
||||
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import { api } from "~/utils/api";
|
||||
import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers";
|
||||
import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation";
|
||||
import ActivityList from "./components/ActivityList";
|
||||
import { AttachmentThumbnails } from "./components/AttachmentThumbnails";
|
||||
import { AttachmentUpload } from "./components/AttachmentUpload";
|
||||
import Checklists from "./components/Checklists";
|
||||
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
||||
import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation";
|
||||
@@ -105,7 +107,7 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
|
||||
|
||||
return (
|
||||
<div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
|
||||
<div className="mb-4 flex w-full flex-row">
|
||||
<div className="mb-4 flex w-full flex-row pt-[18px]">
|
||||
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p>
|
||||
<ListSelector
|
||||
cardPublicId={cardId ?? ""}
|
||||
@@ -208,6 +210,17 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
}
|
||||
}, [card, getModalState, clearModalState]);
|
||||
|
||||
// Auto-resize title textarea
|
||||
useEffect(() => {
|
||||
const titleTextarea = document.getElementById(
|
||||
"title",
|
||||
) as HTMLTextAreaElement;
|
||||
if (titleTextarea) {
|
||||
titleTextarea.style.height = "auto";
|
||||
titleTextarea.style.height = `${titleTextarea.scrollHeight}px`;
|
||||
}
|
||||
}, [card]);
|
||||
|
||||
if (!cardId) return <></>;
|
||||
|
||||
return (
|
||||
@@ -215,47 +228,71 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
<PageHead
|
||||
title={t`${card?.title ?? t`Card`} | ${board?.name ?? t`Board`}`}
|
||||
/>
|
||||
<div className="flex h-full flex-1 flex-row overflow-hidden">
|
||||
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
||||
{/* Full-width top strip with board link and dropdown */}
|
||||
<div className="flex w-full items-center justify-between border-b-[1px] border-light-300 bg-light-50 px-8 py-2 dark:border-dark-300 dark:bg-dark-50">
|
||||
{!card && isLoading && (
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-[1.5rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||
</div>
|
||||
)}
|
||||
{card && (
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<Link
|
||||
className="whitespace-nowrapleading-[1.5rem] text-sm font-bold text-light-900 dark:text-dark-950"
|
||||
href={`${isTemplate ? "/templates" : "/boards"}`}
|
||||
>
|
||||
{workspace.name}
|
||||
</Link>
|
||||
<IoChevronForwardSharp className="h-[10px] w-[10px] text-light-900 dark:text-dark-900" />
|
||||
<Link
|
||||
className="whitespace-nowrap text-sm font-bold leading-[1.5rem] text-light-900 dark:text-dark-950"
|
||||
href={`${isTemplate ? "/templates" : "/boards"}/${board?.publicId}`}
|
||||
>
|
||||
{board?.name}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dropdown />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!card && !isLoading && (
|
||||
<p className="block p-0 py-0 font-bold leading-[1.5rem] tracking-tight text-light-900 dark:text-dark-900 sm:text-[1rem]">
|
||||
{t`Card not found`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="scrollbar-thumb-rounded-[4px] scrollbar-track-rounded-[4px] w-full flex-1 overflow-y-auto scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 hover:scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300 dark:hover:scrollbar-thumb-dark-300">
|
||||
<div className="p-auto mx-auto flex h-full w-full max-w-[800px] flex-col">
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="mb-8 flex w-full items-center justify-between md:mt-6">
|
||||
<div className="mb-8 md:mt-4">
|
||||
{!card && isLoading && (
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||
<div className="h-[2.3rem] w-[300px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||
</div>
|
||||
)}
|
||||
{card && (
|
||||
<>
|
||||
<Link
|
||||
className="whitespace-nowrap font-bold leading-[2.3rem] tracking-tight text-light-900 dark:text-dark-900 sm:text-[1.2rem]"
|
||||
href={`${isTemplate ? "/templates" : "/boards"}/${board?.publicId}`}
|
||||
>
|
||||
{board?.name}
|
||||
</Link>
|
||||
<IoChevronForwardSharp
|
||||
size={18}
|
||||
className="mx-2 text-light-900 dark:text-dark-900"
|
||||
/>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full space-y-6"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
{...register("title")}
|
||||
onBlur={handleSubmit(onSubmit)}
|
||||
className="block w-full border-0 bg-transparent p-0 py-0 font-bold tracking-tight text-neutral-900 focus:ring-0 dark:text-dark-1000 sm:text-[1.2rem]"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex">
|
||||
<Dropdown />
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full space-y-6"
|
||||
>
|
||||
<div>
|
||||
<textarea
|
||||
id="title"
|
||||
{...register("title")}
|
||||
onBlur={handleSubmit(onSubmit)}
|
||||
rows={1}
|
||||
className="block w-full resize-none overflow-hidden border-0 bg-transparent p-0 py-0 font-bold leading-relaxed text-neutral-900 focus:ring-0 dark:text-dark-1000 sm:text-[1.2rem]"
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
target.style.height = "auto";
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</form>
|
||||
)}
|
||||
{!card && !isLoading && (
|
||||
<p className="block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||
@@ -286,6 +323,21 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
activeChecklistForm={activeChecklistForm}
|
||||
setActiveChecklistForm={setActiveChecklistForm}
|
||||
/>
|
||||
{!isTemplate && (
|
||||
<>
|
||||
{card?.attachments.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<AttachmentThumbnails
|
||||
attachments={card.attachments}
|
||||
cardPublicId={cardId ?? ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<AttachmentUpload cardPublicId={cardId} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="border-t-[1px] border-light-300 pt-12 dark:border-dark-300">
|
||||
<h2 className="text-md pb-4 font-medium text-light-1000 dark:text-dark-1000">
|
||||
{t`Activity`}
|
||||
|
||||
@@ -9,6 +9,7 @@ import LabelIcon from "~/components/LabelIcon";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { api } from "~/utils/api";
|
||||
import ActivityList from "~/views/card/components/ActivityList";
|
||||
import { AttachmentThumbnails } from "~/views/card/components/AttachmentThumbnails";
|
||||
import Checklists from "~/views/card/components/Checklists";
|
||||
|
||||
export function CardModal({
|
||||
@@ -66,22 +67,23 @@ export function CardModal({
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<button
|
||||
className="absolute right-[2rem] top-[2rem] rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
|
||||
onClick={async (e) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
closeModal();
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await router.replace(
|
||||
`/${workspaceSlug}/${boardSlug}`,
|
||||
undefined,
|
||||
{
|
||||
shallow: true,
|
||||
setTimeout(() => {
|
||||
void router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
workspaceSlug,
|
||||
boardSlug: [boardSlug],
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}, 400);
|
||||
}}
|
||||
>
|
||||
@@ -126,11 +128,24 @@ export function CardModal({
|
||||
<Editor
|
||||
content={data.description}
|
||||
readOnly
|
||||
workspaceMembers={data?.list.board.workspace.members ?? []}
|
||||
workspaceMembers={
|
||||
data?.list.board.workspace.members ?? []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.attachments &&
|
||||
data.attachments.length > 0 &&
|
||||
cardPublicId && (
|
||||
<div className="mb-10 max-w-2xl">
|
||||
<AttachmentThumbnails
|
||||
attachments={data.attachments}
|
||||
cardPublicId={cardPublicId}
|
||||
isReadOnly
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{data?.checklists && data.checklists.length > 0 && (
|
||||
<Checklists
|
||||
checklists={data.checklists}
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function PublicBoardView() {
|
||||
workspaceSlug: workspaceSlug ?? "",
|
||||
members: formatToArray(router.query.members),
|
||||
labels: formatToArray(router.query.labels),
|
||||
lists: formatToArray(router.query.lists),
|
||||
},
|
||||
{
|
||||
enabled: router.isReady && !!boardSlug,
|
||||
@@ -75,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;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,6 +132,7 @@ export default function PublicBoardView() {
|
||||
<Filters
|
||||
labels={data.labels ?? []}
|
||||
members={[]}
|
||||
lists={data.allLists ?? []}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
@@ -170,26 +173,36 @@ export default function PublicBoardView() {
|
||||
</span>
|
||||
</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">
|
||||
{list.cards.map((card) => (
|
||||
<Link
|
||||
key={card.publicId}
|
||||
href={`/${data.workspace.slug}/${data.slug}/${card.publicId}`}
|
||||
className={`mb-2 flex !cursor-pointer flex-col`}
|
||||
shallow={true}
|
||||
onClick={() => {
|
||||
openModal("CARD");
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
title={card.title}
|
||||
labels={card.labels}
|
||||
checklists={card.checklists ?? []}
|
||||
members={[]}
|
||||
description={card.description}
|
||||
comments={card.comments ?? []}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
{list.cards.map((card) => {
|
||||
return (
|
||||
<Link
|
||||
key={card.publicId}
|
||||
href={{
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
workspaceSlug: data.workspace.slug,
|
||||
boardSlug: [data.slug, card.publicId],
|
||||
},
|
||||
}}
|
||||
className={`mb-2 flex !cursor-pointer flex-col`}
|
||||
shallow={true}
|
||||
onClick={() => {
|
||||
openModal("CARD");
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
title={card.title}
|
||||
labels={card.labels}
|
||||
checklists={card.checklists ?? []}
|
||||
members={[]}
|
||||
description={card.description}
|
||||
comments={card.comments ?? []}
|
||||
attachments={card.attachments}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -35,8 +35,11 @@ services:
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
- PLUNK_API_KEY=${PLUNK_API_KEY}
|
||||
- PLUNK_API_URL=${PLUNK_API_URL}
|
||||
|
||||
# Notifications
|
||||
- NOVU_API_KEY=${NOVU_API_KEY}
|
||||
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
|
||||
- EMAIL_UNSUBSCRIBE_SECRET=${EMAIL_UNSUBSCRIBE_SECRET}
|
||||
|
||||
# S3 storage
|
||||
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
|
||||
@@ -46,6 +49,7 @@ services:
|
||||
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE}
|
||||
- NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL}
|
||||
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
||||
- NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME=${NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME}
|
||||
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
||||
|
||||
# Auth config
|
||||
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE}
|
||||
- NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL}
|
||||
- NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME}
|
||||
- NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME=${NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME}
|
||||
- NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN}
|
||||
|
||||
# White label
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.802.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.812.0",
|
||||
"@kan/auth": "workspace:*",
|
||||
"@kan/db": "workspace:*",
|
||||
"@kan/email": "workspace:^",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { attachmentRouter } from "./routers/attachment";
|
||||
import { boardRouter } from "./routers/board";
|
||||
import { cardRouter } from "./routers/card";
|
||||
import { checklistRouter } from "./routers/checklist";
|
||||
@@ -12,6 +13,7 @@ import { workspaceRouter } from "./routers/workspace";
|
||||
import { createTRPCRouter } from "./trpc";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
attachment: attachmentRouter,
|
||||
board: boardRouter,
|
||||
card: cardRouter,
|
||||
checklist: checklistRouter,
|
||||
|
||||
205
packages/api/src/routers/attachment.ts
Normal file
205
packages/api/src/routers/attachment.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import * as cardRepo from "@kan/db/repository/card.repo";
|
||||
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
|
||||
import * as cardAttachmentRepo from "@kan/db/repository/cardAttachment.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import { generateUID } from "@kan/shared/utils";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { assertUserInWorkspace } from "../utils/auth";
|
||||
import { generateUploadUrl } from "../utils/s3";
|
||||
|
||||
export const attachmentRouter = createTRPCRouter({
|
||||
generateUploadUrl: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Generate presigned URL for attachment upload",
|
||||
method: "POST",
|
||||
path: "/cards/{cardPublicId}/attachments/upload-url",
|
||||
description:
|
||||
"Generates a presigned URL for uploading an attachment to S3",
|
||||
tags: ["Attachments"],
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
filename: z.string().min(1).max(255),
|
||||
contentType: z.string(),
|
||||
size: z
|
||||
.number()
|
||||
.positive()
|
||||
.max(50 * 1024 * 1024), // 50MB max
|
||||
}),
|
||||
)
|
||||
.output(z.object({ url: z.string(), key: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId)
|
||||
throw new TRPCError({
|
||||
message: `User not authenticated`,
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
|
||||
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
|
||||
ctx.db,
|
||||
input.cardPublicId,
|
||||
);
|
||||
|
||||
if (!card)
|
||||
throw new TRPCError({
|
||||
message: `Card with public ID ${input.cardPublicId} not found`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
|
||||
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
|
||||
|
||||
// Get workspace publicId
|
||||
const workspace = await workspaceRepo.getById(ctx.db, card.workspaceId);
|
||||
if (!workspace)
|
||||
throw new TRPCError({
|
||||
message: `Workspace not found`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
|
||||
const bucket = process.env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
|
||||
if (!bucket)
|
||||
throw new TRPCError({
|
||||
message: `Attachments bucket not configured`,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
// Sanitize filename
|
||||
const sanitizedFilename = input.filename
|
||||
.replace(/[^a-zA-Z0-9._-]/g, "_")
|
||||
.substring(0, 200);
|
||||
|
||||
const s3Key = `${workspace.publicId}/${input.cardPublicId}/${generateUID()}-${sanitizedFilename}`;
|
||||
|
||||
const url = await generateUploadUrl(
|
||||
bucket,
|
||||
s3Key,
|
||||
input.contentType,
|
||||
3600, // 1 hour
|
||||
);
|
||||
|
||||
return { url, key: s3Key };
|
||||
}),
|
||||
confirm: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Confirm attachment upload and save to database",
|
||||
method: "POST",
|
||||
path: "/cards/{cardPublicId}/attachments/confirm",
|
||||
description:
|
||||
"Confirms an attachment upload and saves the record to the database",
|
||||
tags: ["Attachments"],
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
s3Key: z.string(),
|
||||
filename: z.string(),
|
||||
originalFilename: z.string(),
|
||||
contentType: z.string(),
|
||||
size: z.number().positive(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof cardAttachmentRepo.create>>>())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId)
|
||||
throw new TRPCError({
|
||||
message: `User not authenticated`,
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
|
||||
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
|
||||
ctx.db,
|
||||
input.cardPublicId,
|
||||
);
|
||||
|
||||
if (!card)
|
||||
throw new TRPCError({
|
||||
message: `Card with public ID ${input.cardPublicId} not found`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
|
||||
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
|
||||
|
||||
const attachment = await cardAttachmentRepo.create(ctx.db, {
|
||||
cardId: card.id,
|
||||
filename: input.filename,
|
||||
originalFilename: input.originalFilename,
|
||||
contentType: input.contentType,
|
||||
size: input.size,
|
||||
s3Key: input.s3Key,
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
await cardActivityRepo.create(ctx.db, {
|
||||
type: "card.updated.attachment.added",
|
||||
cardId: card.id,
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
return attachment;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete an attachment",
|
||||
method: "DELETE",
|
||||
path: "/attachments/{attachmentPublicId}",
|
||||
description: "Soft deletes an attachment",
|
||||
tags: ["Attachments"],
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(z.object({ attachmentPublicId: z.string().min(12) }))
|
||||
.output(z.object({ success: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId)
|
||||
throw new TRPCError({
|
||||
message: `User not authenticated`,
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
|
||||
const attachment = await cardAttachmentRepo.getByPublicId(
|
||||
ctx.db,
|
||||
input.attachmentPublicId,
|
||||
);
|
||||
|
||||
if (!attachment || attachment.deletedAt)
|
||||
throw new TRPCError({
|
||||
message: `Attachment with public ID ${input.attachmentPublicId} not found`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
|
||||
const workspaceId = attachment.card.list.board.workspaceId;
|
||||
|
||||
await assertUserInWorkspace(ctx.db, userId, workspaceId);
|
||||
|
||||
await cardAttachmentRepo.softDelete(ctx.db, {
|
||||
attachmentId: attachment.id,
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
await cardActivityRepo.create(ctx.db, {
|
||||
type: "card.updated.attachment.removed",
|
||||
cardId: attachment.cardId,
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
@@ -78,6 +78,7 @@ export const boardRouter = createTRPCRouter({
|
||||
boardPublicId: z.string().min(12),
|
||||
members: z.array(z.string().min(12)).optional(),
|
||||
labels: z.array(z.string().min(12)).optional(),
|
||||
lists: z.array(z.string().min(12)).optional(),
|
||||
type: z.enum(["regular", "template"]).optional(),
|
||||
}),
|
||||
)
|
||||
@@ -110,6 +111,7 @@ export const boardRouter = createTRPCRouter({
|
||||
{
|
||||
members: input.members ?? [],
|
||||
labels: input.labels ?? [],
|
||||
lists: input.lists ?? [],
|
||||
type: input.type,
|
||||
},
|
||||
);
|
||||
@@ -142,6 +144,7 @@ export const boardRouter = createTRPCRouter({
|
||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
|
||||
members: z.array(z.string().min(12)).optional(),
|
||||
labels: z.array(z.string().min(12)).optional(),
|
||||
lists: z.array(z.string().min(12)).optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof boardRepo.getBySlug>>>())
|
||||
@@ -164,6 +167,7 @@ export const boardRouter = createTRPCRouter({
|
||||
{
|
||||
members: input.members ?? [],
|
||||
labels: input.labels ?? [],
|
||||
lists: input.lists ?? [],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -234,6 +238,7 @@ export const boardRouter = createTRPCRouter({
|
||||
{
|
||||
members: [],
|
||||
labels: [],
|
||||
lists: [],
|
||||
type: sourceBoardInfo.type,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { assertUserInWorkspace } from "../utils/auth";
|
||||
import { generateDownloadUrl } from "../utils/s3";
|
||||
|
||||
export const cardRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -574,7 +575,21 @@ export const cardRouter = createTRPCRouter({
|
||||
.input(z.object({ cardPublicId: z.string().min(12) }))
|
||||
.output(
|
||||
z.custom<
|
||||
Awaited<ReturnType<typeof cardRepo.getWithListAndMembersByPublicId>>
|
||||
Omit<
|
||||
NonNullable<
|
||||
Awaited<ReturnType<typeof cardRepo.getWithListAndMembersByPublicId>>
|
||||
>,
|
||||
"attachments"
|
||||
> & {
|
||||
attachments: {
|
||||
publicId: string;
|
||||
contentType: string;
|
||||
s3Key: string;
|
||||
originalFilename: string | null;
|
||||
size?: number | null;
|
||||
url: string | null;
|
||||
}[];
|
||||
}
|
||||
>(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
@@ -612,7 +627,46 @@ export const cardRouter = createTRPCRouter({
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
|
||||
return result;
|
||||
// Generate URLs for all attachments
|
||||
const bucket = process.env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
|
||||
if (result.attachments && Array.isArray(result.attachments)) {
|
||||
const attachments = result.attachments as {
|
||||
publicId: string;
|
||||
contentType: string;
|
||||
s3Key: string;
|
||||
originalFilename: string | null;
|
||||
size?: number | null;
|
||||
}[];
|
||||
|
||||
const attachmentsWithUrls = await Promise.all(
|
||||
attachments.map(async (attachment) => {
|
||||
const base = {
|
||||
publicId: attachment.publicId,
|
||||
contentType: attachment.contentType,
|
||||
s3Key: attachment.s3Key,
|
||||
originalFilename: attachment.originalFilename,
|
||||
size: attachment.size,
|
||||
};
|
||||
if (!bucket || !attachment.s3Key) {
|
||||
return { ...base, url: null };
|
||||
}
|
||||
try {
|
||||
const url = await generateDownloadUrl(
|
||||
bucket,
|
||||
attachment.s3Key,
|
||||
86400, // 24 hours expiration
|
||||
);
|
||||
return { ...base, url };
|
||||
} catch {
|
||||
// If URL generation fails, return attachment with url: null
|
||||
return { ...base, url: null };
|
||||
}
|
||||
}),
|
||||
);
|
||||
return { ...result, attachments: attachmentsWithUrls };
|
||||
}
|
||||
|
||||
return { ...result, attachments: [] };
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.meta({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import * as boardRepo from "@kan/db/repository/board.repo";
|
||||
import * as cardRepo from "@kan/db/repository/card.repo";
|
||||
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
|
||||
import * as checklistRepo from "@kan/db/repository/checklist.repo";
|
||||
import * as importRepo from "@kan/db/repository/import.repo";
|
||||
import * as integrationsRepo from "@kan/db/repository/integration.repo";
|
||||
import * as labelRepo from "@kan/db/repository/label.repo";
|
||||
@@ -22,6 +23,7 @@ export interface TrelloBoard {
|
||||
labels: TrelloLabel[];
|
||||
lists: TrelloList[];
|
||||
cards: TrelloCard[];
|
||||
checklists: TrelloChecklist[];
|
||||
}
|
||||
|
||||
interface TrelloLabel {
|
||||
@@ -34,12 +36,34 @@ interface TrelloList {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface TrelloChecklist {
|
||||
id: string;
|
||||
idCard: string;
|
||||
name: string;
|
||||
checkItems: TrelloCheckItem[];
|
||||
}
|
||||
|
||||
interface TrelloCheckItem {
|
||||
id: string;
|
||||
name: string;
|
||||
state: "complete" | "incomplete";
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface TrelloCard {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
idList: string;
|
||||
labels: TrelloLabel[];
|
||||
idChecklists: string[];
|
||||
checkItemStates: TrelloCheckItemState[];
|
||||
}
|
||||
|
||||
interface TrelloCheckItemState {
|
||||
idChecklist: string;
|
||||
idCheckItem: string;
|
||||
state: "complete" | "incomplete";
|
||||
}
|
||||
|
||||
export const importRouter = createTRPCRouter({
|
||||
@@ -170,7 +194,7 @@ export const importRouter = createTRPCRouter({
|
||||
|
||||
for (const boardId of input.boardIds) {
|
||||
const response = await fetch(
|
||||
`${urls.trello}/boards/${boardId}?key=${apiKey}&token=${integration.accessToken}&lists=open&cards=open&labels=all`,
|
||||
`${urls.trello}/boards/${boardId}?key=${apiKey}&token=${integration.accessToken}&lists=open&cards=open&labels=all&checklists=all&checkItemStates=all`,
|
||||
);
|
||||
const data = (await response.json()) as TrelloBoard;
|
||||
|
||||
@@ -194,6 +218,18 @@ export const importRouter = createTRPCRouter({
|
||||
sourceId: label.id,
|
||||
name: label.name,
|
||||
})),
|
||||
checklists: data.checklists
|
||||
.filter((checklist) => checklist.idCard === _card.id)
|
||||
.map((_checklist) => ({
|
||||
sourceId: _checklist.id,
|
||||
name: _checklist.name,
|
||||
items: _checklist.checkItems.map((_item) => ({
|
||||
sourceId: _item.id,
|
||||
title: _item.name,
|
||||
completed: _item.state === "complete",
|
||||
index: _item.pos,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
@@ -289,6 +325,97 @@ export const importRouter = createTRPCRouter({
|
||||
await cardActivityRepo.bulkCreate(ctx.db, activities);
|
||||
}
|
||||
|
||||
const checklistsToCreate: {
|
||||
cardId: number;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
sourceId: string;
|
||||
items: {
|
||||
sourceId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
index: number;
|
||||
}[];
|
||||
}[] = [];
|
||||
|
||||
for (const card of list.cards) {
|
||||
const _card = createdCards.find(
|
||||
(c) => c.sourceId === card.sourceId,
|
||||
);
|
||||
|
||||
if (!_card || !card.checklists.length) continue;
|
||||
|
||||
for (
|
||||
let checklistIndex = 0;
|
||||
checklistIndex < card.checklists.length;
|
||||
checklistIndex++
|
||||
) {
|
||||
const checklist = card.checklists[checklistIndex];
|
||||
if (!checklist) continue;
|
||||
|
||||
checklistsToCreate.push({
|
||||
cardId: _card.id,
|
||||
name: checklist.name,
|
||||
createdBy: userId,
|
||||
index: checklistIndex,
|
||||
sourceId: checklist.sourceId,
|
||||
items: checklist.items.map((item) => ({
|
||||
sourceId: item.sourceId,
|
||||
title: item.title,
|
||||
completed: item.completed,
|
||||
index: item.index,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (checklistsToCreate.length > 0) {
|
||||
const newChecklists = await checklistRepo.bulkCreate(
|
||||
ctx.db,
|
||||
checklistsToCreate.map((checklist) => ({
|
||||
cardId: checklist.cardId,
|
||||
name: checklist.name,
|
||||
createdBy: checklist.createdBy,
|
||||
index: checklist.index,
|
||||
})),
|
||||
);
|
||||
|
||||
const itemsToCreate: {
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
completed: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < checklistsToCreate.length; i++) {
|
||||
const checklistData = checklistsToCreate[i];
|
||||
const newChecklist = newChecklists[i];
|
||||
|
||||
if (!newChecklist || !checklistData?.items.length) continue;
|
||||
|
||||
// NOTE: Sorting here to prevent checklist items being out of order
|
||||
const sortedItems = [...checklistData.items].sort(
|
||||
(a, b) => a.index - b.index,
|
||||
);
|
||||
|
||||
for (const item of sortedItems) {
|
||||
itemsToCreate.push({
|
||||
checklistId: newChecklist.id,
|
||||
title: item.title,
|
||||
createdBy: userId,
|
||||
index: item.index,
|
||||
completed: item.completed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsToCreate.length > 0) {
|
||||
await checklistRepo.bulkCreateItems(ctx.db, itemsToCreate);
|
||||
}
|
||||
}
|
||||
|
||||
if (createdLabels.length && createdCards.length) {
|
||||
const cardLabelRelations: {
|
||||
cardId: number;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { env } from "next-runtime-env";
|
||||
import { z } from "zod";
|
||||
|
||||
import * as inviteLinkRepo from "@kan/db/repository/inviteLink.repo";
|
||||
@@ -307,7 +308,7 @@ export const memberRouter = createTRPCRouter({
|
||||
return {
|
||||
id: activeInviteLink.id,
|
||||
inviteCode: activeInviteLink.code,
|
||||
inviteLink: `${process.env.NEXT_PUBLIC_BASE_URL}/invite/${activeInviteLink.code}`,
|
||||
inviteLink: `${env("NEXT_PUBLIC_BASE_URL")}/invite/${activeInviteLink.code}`,
|
||||
isActive: true,
|
||||
expiresAt: activeInviteLink.expiresAt ?? undefined,
|
||||
};
|
||||
@@ -415,7 +416,7 @@ export const memberRouter = createTRPCRouter({
|
||||
return {
|
||||
publicId: inviteLink.publicId,
|
||||
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,
|
||||
};
|
||||
}),
|
||||
@@ -476,7 +477,7 @@ export const memberRouter = createTRPCRouter({
|
||||
openapi: {
|
||||
summary: "Get invite information by code",
|
||||
method: "GET",
|
||||
path: "/workspaces/{workspacePublicId}/invites/{inviteCode}",
|
||||
path: "/invites/{inviteCode}",
|
||||
description: "Get invite information by invite code",
|
||||
tags: ["Invites"],
|
||||
protect: false,
|
||||
@@ -521,7 +522,7 @@ export const memberRouter = createTRPCRouter({
|
||||
openapi: {
|
||||
summary: "Accept an invite link",
|
||||
method: "POST",
|
||||
path: "/workspaces/{workspacePublicId}/invites/accept",
|
||||
path: "/invites/accept",
|
||||
description: "Accepts an invitation via invite link",
|
||||
tags: ["Invites"],
|
||||
protect: false,
|
||||
|
||||
53
packages/api/src/utils/s3.ts
Normal file
53
packages/api/src/utils/s3.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
export function createS3Client() {
|
||||
return new S3Client({
|
||||
region: process.env.S3_REGION ?? "",
|
||||
endpoint: process.env.S3_ENDPOINT ?? "",
|
||||
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID ?? "",
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY ?? "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateUploadUrl(
|
||||
bucket: string,
|
||||
key: string,
|
||||
contentType: string,
|
||||
expiresIn = 3600,
|
||||
) {
|
||||
const client = createS3Client();
|
||||
return getSignedUrl(
|
||||
client,
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
// Don't set ACL for private files
|
||||
}),
|
||||
{ expiresIn },
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateDownloadUrl(
|
||||
bucket: string,
|
||||
key: string,
|
||||
expiresIn = 3600,
|
||||
) {
|
||||
const client = createS3Client();
|
||||
return getSignedUrl(
|
||||
client,
|
||||
new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
}),
|
||||
{ expiresIn },
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Subscription } from "@better-auth/stripe";
|
||||
import type Stripe from "stripe";
|
||||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { stripe } from "@better-auth/stripe";
|
||||
import { ChatOrPushProviderEnum } from "@novu/api/models/components";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
|
||||
@@ -14,7 +17,8 @@ import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
||||
import * as userRepo from "@kan/db/repository/user.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import * as schema from "@kan/db/schema";
|
||||
import { cloudMailerClient, sendEmail } from "@kan/email";
|
||||
import { notificationClient, sendEmail } from "@kan/email";
|
||||
import { createEmailUnsubscribeLink } from "@kan/shared";
|
||||
import { createStripeClient } from "@kan/stripe";
|
||||
|
||||
export const configuredProviders = socialProviderList.reduce<
|
||||
@@ -129,6 +133,11 @@ export const initAuth = (db: dbClient) => {
|
||||
user: schema.users,
|
||||
},
|
||||
}),
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
||||
updateAge: 60 * 60 * 24 * 2, // Update session expiry every 48 hours if user is active
|
||||
freshAge: 0,
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true",
|
||||
disableSignUp:
|
||||
@@ -173,19 +182,17 @@ export const initAuth = (db: dbClient) => {
|
||||
freeTrial: {
|
||||
days: 14,
|
||||
onTrialStart: async (subscription) => {
|
||||
// Called when a trial starts
|
||||
// @todo: send trial start email
|
||||
// await sendTrialStartEmail(subscription.referenceId);
|
||||
await triggerWorkflow(db, "trial-start", subscription);
|
||||
},
|
||||
onTrialEnd: async ({ subscription }, request) => {
|
||||
// Called when a trial ends
|
||||
// @todo: send trial end email
|
||||
// await sendTrialEndEmail(user.email);
|
||||
onTrialEnd: async ({ subscription }) => {
|
||||
await triggerWorkflow(db, "trial-end", subscription);
|
||||
},
|
||||
onTrialExpired: async (subscription) => {
|
||||
// Called when a trial expires without conversion
|
||||
// @todo: send trial expired email
|
||||
// await sendTrialExpiredEmail(subscription.referenceId);
|
||||
await triggerWorkflow(
|
||||
db,
|
||||
"trial-expired",
|
||||
subscription,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -197,19 +204,17 @@ export const initAuth = (db: dbClient) => {
|
||||
freeTrial: {
|
||||
days: 14,
|
||||
onTrialStart: async (subscription) => {
|
||||
// Called when a trial starts
|
||||
// @todo: send trial start email
|
||||
// await sendTrialStartEmail(subscription.referenceId);
|
||||
await triggerWorkflow(db, "trial-start", subscription);
|
||||
},
|
||||
onTrialEnd: async ({ subscription }, request) => {
|
||||
// Called when a trial ends
|
||||
// @todo: send trial end email
|
||||
// await sendTrialEndEmail(user.email);
|
||||
onTrialEnd: async ({ subscription }) => {
|
||||
await triggerWorkflow(db, "trial-end", subscription);
|
||||
},
|
||||
onTrialExpired: async (subscription) => {
|
||||
// Called when a trial expires without conversion
|
||||
// @todo: send trial expired email
|
||||
// await sendTrialExpiredEmail(subscription.referenceId);
|
||||
await triggerWorkflow(
|
||||
db,
|
||||
"trial-expired",
|
||||
subscription,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -343,17 +348,7 @@ export const initAuth = (db: dbClient) => {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
async after(user) {
|
||||
if (cloudMailerClient) {
|
||||
await cloudMailerClient.events.track({
|
||||
event: "user-signup",
|
||||
email: user.email,
|
||||
data: {
|
||||
name: user.name,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let avatarKey = user.image;
|
||||
if (
|
||||
user.image &&
|
||||
!user.image.includes(process.env.NEXT_PUBLIC_STORAGE_DOMAIN!)
|
||||
@@ -386,6 +381,9 @@ export const initAuth = (db: dbClient) => {
|
||||
ACL: "public-read",
|
||||
}),
|
||||
);
|
||||
|
||||
avatarKey = key;
|
||||
|
||||
await userRepo.update(db, user.id, {
|
||||
image: key,
|
||||
});
|
||||
@@ -393,6 +391,58 @@ export const initAuth = (db: dbClient) => {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationClient) {
|
||||
try {
|
||||
const [firstName, ...rest] = user.name
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
const lastName = rest.length ? rest.join(" ") : undefined;
|
||||
const avatarUrl = avatarKey
|
||||
? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}`
|
||||
: undefined;
|
||||
|
||||
const unsubscribeUrl = await createEmailUnsubscribeLink(
|
||||
user.id,
|
||||
);
|
||||
|
||||
await notificationClient.trigger({
|
||||
to: {
|
||||
subscriberId: user.id,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
email: user.email,
|
||||
avatar: avatarUrl,
|
||||
data: {
|
||||
emailVerified: user.emailVerified,
|
||||
stripeCustomerId: user.stripeCustomerId,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
emailUnsubscribeUrl: unsubscribeUrl,
|
||||
},
|
||||
workflowId: "user-signup",
|
||||
});
|
||||
|
||||
await notificationClient.subscribers.credentials.update(
|
||||
{
|
||||
providerId: ChatOrPushProviderEnum.Discord,
|
||||
credentials: {
|
||||
webhookUrl: process.env.DISCORD_WEBHOOK_URL!,
|
||||
},
|
||||
integrationIdentifier: "discord",
|
||||
},
|
||||
user.id,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error adding user to notification client",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -430,3 +480,37 @@ export const initAuth = (db: dbClient) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function triggerWorkflow(
|
||||
db: dbClient,
|
||||
workflowId: string,
|
||||
subscription: Subscription,
|
||||
cancellationDetails?: Stripe.Subscription.CancellationDetails | null,
|
||||
) {
|
||||
try {
|
||||
if (!subscription.stripeCustomerId || !notificationClient) return;
|
||||
|
||||
const user = await userRepo.getByStripeCustomerId(
|
||||
db,
|
||||
subscription.stripeCustomerId,
|
||||
);
|
||||
|
||||
if (!user || !notificationClient) return;
|
||||
|
||||
const unsubscribeUrl = await createEmailUnsubscribeLink(user.id);
|
||||
|
||||
await notificationClient.trigger({
|
||||
to: {
|
||||
subscriberId: user.id,
|
||||
},
|
||||
payload: {
|
||||
...subscription,
|
||||
cancellationDetails,
|
||||
emailUnsubscribeUrl: unsubscribeUrl,
|
||||
},
|
||||
workflowId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error triggering workflow", error);
|
||||
}
|
||||
}
|
||||
|
||||
29
packages/db/migrations/20251110204423_AddCardAttachments.sql
Normal file
29
packages/db/migrations/20251110204423_AddCardAttachments.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.attachment.added' BEFORE 'card.archived';--> statement-breakpoint
|
||||
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.attachment.removed' BEFORE 'card.archived';--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "card_attachment" (
|
||||
"id" bigserial PRIMARY KEY NOT NULL,
|
||||
"publicId" varchar(12) NOT NULL,
|
||||
"cardId" bigint NOT NULL,
|
||||
"filename" varchar(255) NOT NULL,
|
||||
"originalFilename" varchar(255) NOT NULL,
|
||||
"contentType" varchar(100) NOT NULL,
|
||||
"size" bigint NOT NULL,
|
||||
"s3Key" varchar(500) NOT NULL,
|
||||
"createdBy" uuid,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp,
|
||||
CONSTRAINT "card_attachment_publicId_unique" UNIQUE("publicId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "card_attachment" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "card_attachment" ADD CONSTRAINT "card_attachment_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "public"."card"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "card_attachment" ADD CONSTRAINT "card_attachment_createdBy_user_id_fk" FOREIGN KEY ("createdBy") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
2954
packages/db/migrations/meta/20251110204423_snapshot.json
Normal file
2954
packages/db/migrations/meta/20251110204423_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,13 @@
|
||||
"when": 1760044396109,
|
||||
"tag": "20251009211316_AddBoardSourceIdToCardActivity",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "7",
|
||||
"when": 1762807463569,
|
||||
"tag": "20251110204423_AddCardAttachments",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { BoardVisibilityStatus } from "@kan/db/schema";
|
||||
import {
|
||||
boards,
|
||||
cardActivities,
|
||||
cardAttachments,
|
||||
cards,
|
||||
cardsToLabels,
|
||||
cardToWorkspaceMembers,
|
||||
@@ -70,6 +71,7 @@ export const getByPublicId = async (
|
||||
filters: {
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
type: "regular" | "template" | undefined;
|
||||
},
|
||||
) => {
|
||||
@@ -195,6 +197,13 @@ export const getByPublicId = async (
|
||||
},
|
||||
},
|
||||
},
|
||||
attachments: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
where: isNull(cardAttachments.deletedAt),
|
||||
orderBy: asc(cardAttachments.createdAt),
|
||||
},
|
||||
checklists: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
@@ -231,6 +240,19 @@ export const getByPublicId = async (
|
||||
orderBy: [asc(cards.index)],
|
||||
},
|
||||
},
|
||||
where: and(
|
||||
isNull(lists.deletedAt),
|
||||
filters.lists.length > 0
|
||||
? inArray(lists.publicId, filters.lists)
|
||||
: undefined,
|
||||
),
|
||||
orderBy: [asc(lists.index)],
|
||||
},
|
||||
allLists: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
where: isNull(lists.deletedAt),
|
||||
orderBy: [asc(lists.index)],
|
||||
},
|
||||
@@ -268,6 +290,7 @@ export const getBySlug = async (
|
||||
filters: {
|
||||
members: string[];
|
||||
labels: string[];
|
||||
lists: string[];
|
||||
},
|
||||
) => {
|
||||
let cardIds: string[] = [];
|
||||
@@ -343,6 +366,13 @@ export const getBySlug = async (
|
||||
},
|
||||
},
|
||||
},
|
||||
attachments: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
where: isNull(cardAttachments.deletedAt),
|
||||
orderBy: asc(cardAttachments.createdAt),
|
||||
},
|
||||
comments: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
@@ -379,6 +409,19 @@ export const getBySlug = async (
|
||||
orderBy: [asc(cards.index)],
|
||||
},
|
||||
},
|
||||
where: and(
|
||||
isNull(lists.deletedAt),
|
||||
filters.lists.length > 0
|
||||
? inArray(lists.publicId, filters.lists)
|
||||
: undefined,
|
||||
),
|
||||
orderBy: [asc(lists.index)],
|
||||
},
|
||||
allLists: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
where: isNull(lists.deletedAt),
|
||||
orderBy: [asc(lists.index)],
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { and, asc, desc, eq, gt, inArray, isNull, sql } from "drizzle-orm";
|
||||
import type { dbClient } from "@kan/db/client";
|
||||
import {
|
||||
cardActivities,
|
||||
cardAttachments,
|
||||
cards,
|
||||
cardsToLabels,
|
||||
cardToWorkspaceMembers,
|
||||
@@ -409,6 +410,17 @@ export const getWithListAndMembersByPublicId = async (
|
||||
},
|
||||
},
|
||||
},
|
||||
attachments: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
contentType: true,
|
||||
s3Key: true,
|
||||
originalFilename: true,
|
||||
size: true,
|
||||
},
|
||||
where: isNull(cardAttachments.deletedAt),
|
||||
orderBy: asc(cardAttachments.createdAt),
|
||||
},
|
||||
checklists: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
|
||||
99
packages/db/src/repository/cardAttachment.repo.ts
Normal file
99
packages/db/src/repository/cardAttachment.repo.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
|
||||
import type { dbClient } from "@kan/db/client";
|
||||
import { cardAttachments } from "@kan/db/schema";
|
||||
import { generateUID } from "@kan/shared/utils";
|
||||
|
||||
export const create = async (
|
||||
db: dbClient,
|
||||
attachmentInput: {
|
||||
cardId: number;
|
||||
filename: string;
|
||||
originalFilename: string;
|
||||
contentType: string;
|
||||
size: number;
|
||||
s3Key: string;
|
||||
createdBy: string;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
.insert(cardAttachments)
|
||||
.values({
|
||||
publicId: generateUID(),
|
||||
cardId: attachmentInput.cardId,
|
||||
filename: attachmentInput.filename,
|
||||
originalFilename: attachmentInput.originalFilename,
|
||||
contentType: attachmentInput.contentType,
|
||||
size: attachmentInput.size,
|
||||
s3Key: attachmentInput.s3Key,
|
||||
createdBy: attachmentInput.createdBy,
|
||||
})
|
||||
.returning({
|
||||
id: cardAttachments.id,
|
||||
publicId: cardAttachments.publicId,
|
||||
filename: cardAttachments.filename,
|
||||
originalFilename: cardAttachments.originalFilename,
|
||||
contentType: cardAttachments.contentType,
|
||||
size: cardAttachments.size,
|
||||
s3Key: cardAttachments.s3Key,
|
||||
createdBy: cardAttachments.createdBy,
|
||||
createdAt: cardAttachments.createdAt,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getByPublicId = (db: dbClient, publicId: string) => {
|
||||
return db.query.cardAttachments.findFirst({
|
||||
where: eq(cardAttachments.publicId, publicId),
|
||||
with: {
|
||||
card: {
|
||||
columns: {
|
||||
id: true,
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
list: {
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
with: {
|
||||
board: {
|
||||
columns: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllByCardId = (db: dbClient, cardId: number) => {
|
||||
return db.query.cardAttachments.findMany({
|
||||
where: and(
|
||||
eq(cardAttachments.cardId, cardId),
|
||||
isNull(cardAttachments.deletedAt),
|
||||
),
|
||||
orderBy: (attachments, { desc }) => [desc(attachments.createdAt)],
|
||||
});
|
||||
};
|
||||
|
||||
export const softDelete = async (
|
||||
db: dbClient,
|
||||
args: {
|
||||
attachmentId: number;
|
||||
deletedAt: Date;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
.update(cardAttachments)
|
||||
.set({ deletedAt: args.deletedAt })
|
||||
.where(eq(cardAttachments.id, args.attachmentId))
|
||||
.returning({ id: cardAttachments.id });
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -46,6 +46,7 @@ export const createItem = async (
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
completed?: boolean;
|
||||
},
|
||||
) => {
|
||||
return db.transaction(async (tx) => {
|
||||
@@ -65,7 +66,7 @@ export const createItem = async (
|
||||
createdBy: checklistItemInput.createdBy,
|
||||
checklistId: checklistItemInput.checklistId,
|
||||
index: lastItem ? lastItem.index + 1 : 0,
|
||||
completed: false,
|
||||
completed: checklistItemInput.completed ?? false,
|
||||
})
|
||||
.returning({
|
||||
id: checklistItems.id,
|
||||
@@ -214,3 +215,129 @@ export const updateChecklistById = async (
|
||||
.returning({ publicId: checklists.publicId, name: checklists.name });
|
||||
return result;
|
||||
};
|
||||
|
||||
export const bulkCreate = async (
|
||||
db: dbClient,
|
||||
checklistInput: {
|
||||
cardId: number;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
}[],
|
||||
) => {
|
||||
if (checklistInput.length === 0) return [];
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
const byCard = groupByKey(checklistInput, "cardId");
|
||||
|
||||
const allValuesToInsert: {
|
||||
publicId: string;
|
||||
cardId: number;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
}[] = [];
|
||||
|
||||
for (const [cardId, items] of byCard.entries()) {
|
||||
const last = await tx.query.checklists.findFirst({
|
||||
columns: { index: true },
|
||||
where: and(eq(checklists.cardId, cardId), isNull(checklists.deletedAt)),
|
||||
orderBy: [desc(checklists.index)],
|
||||
});
|
||||
|
||||
let nextIndex = last ? last.index + 1 : 0;
|
||||
|
||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||
|
||||
for (const item of sorted) {
|
||||
allValuesToInsert.push({
|
||||
publicId: generateUID(),
|
||||
...item,
|
||||
index: nextIndex++,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const inserted = await tx
|
||||
.insert(checklists)
|
||||
.values(allValuesToInsert)
|
||||
.returning({ id: checklists.id, publicId: checklists.publicId });
|
||||
|
||||
return inserted;
|
||||
});
|
||||
};
|
||||
|
||||
export const bulkCreateItems = async (
|
||||
db: dbClient,
|
||||
checklistItemInput: {
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
completed: boolean;
|
||||
}[],
|
||||
) => {
|
||||
if (checklistItemInput.length === 0) return [];
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
const byChecklist = groupByKey(checklistItemInput, "checklistId");
|
||||
|
||||
const allValuesToInsert: {
|
||||
publicId: string;
|
||||
checklistId: number;
|
||||
title: string;
|
||||
createdBy: string;
|
||||
index: number;
|
||||
completed: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (const [checklistId, items] of byChecklist.entries()) {
|
||||
const last = await tx.query.checklistItems.findFirst({
|
||||
columns: { index: true },
|
||||
where: and(
|
||||
eq(checklistItems.checklistId, checklistId),
|
||||
isNull(checklistItems.deletedAt),
|
||||
),
|
||||
orderBy: [desc(checklistItems.index)],
|
||||
});
|
||||
|
||||
let nextIndex = last ? last.index + 1 : 0;
|
||||
|
||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||
|
||||
for (const item of sorted) {
|
||||
allValuesToInsert.push({
|
||||
publicId: generateUID(),
|
||||
...item,
|
||||
index: nextIndex++,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const inserted = await tx
|
||||
.insert(checklistItems)
|
||||
.values(allValuesToInsert)
|
||||
.returning({
|
||||
id: checklistItems.id,
|
||||
publicId: checklistItems.publicId,
|
||||
title: checklistItems.title,
|
||||
completed: checklistItems.completed,
|
||||
});
|
||||
|
||||
return inserted;
|
||||
});
|
||||
};
|
||||
|
||||
const groupByKey = <T extends Record<string, unknown>>(
|
||||
items: T[],
|
||||
keyField: keyof T,
|
||||
): Map<number, T[]> => {
|
||||
const grouped = new Map<number, T[]>();
|
||||
for (const item of items) {
|
||||
const key = item[keyField] as number;
|
||||
const arr = grouped.get(key) ?? [];
|
||||
arr.push(item);
|
||||
grouped.set(key, arr);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,15 @@ export const getById = async (db: dbClient, userId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getByStripeCustomerId = async (
|
||||
db: dbClient,
|
||||
stripeCustomerId: string,
|
||||
) => {
|
||||
return await db.query.users.findFirst({
|
||||
where: eq(users.stripeCustomerId, stripeCustomerId),
|
||||
});
|
||||
};
|
||||
|
||||
export const getByEmail = (db: dbClient, email: string) => {
|
||||
return db.query.users.findFirst({
|
||||
columns: {
|
||||
|
||||
@@ -73,6 +73,7 @@ export const boardsRelations = relations(boards, ({ one, many }) => ({
|
||||
relationName: "boardCreatedByUser",
|
||||
}),
|
||||
lists: many(lists),
|
||||
allLists: many(lists),
|
||||
labels: many(labels),
|
||||
deletedBy: one(users, {
|
||||
fields: [boards.deletedBy],
|
||||
|
||||
@@ -42,6 +42,8 @@ export const activityTypes = [
|
||||
"card.updated.checklist.item.completed",
|
||||
"card.updated.checklist.item.uncompleted",
|
||||
"card.updated.checklist.item.deleted",
|
||||
"card.updated.attachment.added",
|
||||
"card.updated.attachment.removed",
|
||||
"card.archived",
|
||||
] as const;
|
||||
|
||||
@@ -96,6 +98,7 @@ export const cardsRelations = relations(cards, ({ one, many }) => ({
|
||||
comments: many(comments),
|
||||
activities: many(cardActivities),
|
||||
checklists: many(checklists),
|
||||
attachments: many(cardAttachments),
|
||||
}));
|
||||
|
||||
export const cardActivities = pgTable("card_activity", {
|
||||
@@ -273,3 +276,37 @@ export const commentsRelations = relations(comments, ({ one }) => ({
|
||||
relationName: "commentsDeletedByUser",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const cardAttachments = pgTable("card_attachment", {
|
||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||
cardId: bigint("cardId", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => cards.id, { onDelete: "cascade" }),
|
||||
filename: varchar("filename", { length: 255 }).notNull(),
|
||||
originalFilename: varchar("originalFilename", { length: 255 }).notNull(),
|
||||
contentType: varchar("contentType", { length: 100 }).notNull(),
|
||||
size: bigint("size", { mode: "number" }).notNull(),
|
||||
s3Key: varchar("s3Key", { length: 500 }).notNull(),
|
||||
createdBy: uuid("createdBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
deletedAt: timestamp("deletedAt"),
|
||||
}).enableRLS();
|
||||
|
||||
export const cardAttachmentsRelations = relations(
|
||||
cardAttachments,
|
||||
({ one }) => ({
|
||||
card: one(cards, {
|
||||
fields: [cardAttachments.cardId],
|
||||
references: [cards.id],
|
||||
relationName: "cardAttachmentsCard",
|
||||
}),
|
||||
createdBy: one(users, {
|
||||
fields: [cardAttachments.createdBy],
|
||||
references: [users.id],
|
||||
relationName: "cardAttachmentsCreatedByUser",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plunk/node": "^3.0.3",
|
||||
"@novu/api": "^3.11.0",
|
||||
"@react-email/components": "^0.0.25",
|
||||
"nodemailer": "^7.0.3",
|
||||
"react-email": "^3.0.1"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import Plunk from "@plunk/node";
|
||||
|
||||
export const cloudMailerClient =
|
||||
process.env.NEXT_PUBLIC_KAN_ENV === "cloud" &&
|
||||
process.env.PLUNK_API_KEY &&
|
||||
process.env.PLUNK_API_URL
|
||||
? new Plunk(process.env.PLUNK_API_KEY ?? "", {
|
||||
baseUrl: process.env.PLUNK_API_URL,
|
||||
})
|
||||
: null;
|
||||
@@ -1,4 +1,4 @@
|
||||
export const name = "email";
|
||||
|
||||
export { sendEmail } from "./sendEmail";
|
||||
export { cloudMailerClient } from "./cloudMailerClient";
|
||||
export { notificationClient } from "./notificationClient";
|
||||
|
||||
6
packages/email/src/notificationClient.tsx
Normal file
6
packages/email/src/notificationClient.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Novu } from "@novu/api";
|
||||
|
||||
export const notificationClient =
|
||||
process.env.NEXT_PUBLIC_KAN_ENV === "cloud" && process.env.NOVU_API_KEY
|
||||
? new Novu({ secretKey: process.env.NOVU_API_KEY })
|
||||
: null;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { render } from "@react-email/render";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { cloudMailerClient } from "./cloudMailerClient";
|
||||
import JoinWorkspaceTemplate from "./templates/join-workspace";
|
||||
import MagicLinkTemplate from "./templates/magic-link";
|
||||
import ResetPasswordTemplate from "./templates/reset-password";
|
||||
@@ -45,15 +44,6 @@ export const sendEmail = async (
|
||||
html,
|
||||
};
|
||||
|
||||
// if (cloudMailerClient) {
|
||||
// const response = await cloudMailerClient.emails.send({
|
||||
// ...options,
|
||||
// body: html,
|
||||
// });
|
||||
|
||||
// return response;
|
||||
// }
|
||||
|
||||
const response = await transporter.sendMail(options);
|
||||
|
||||
if (!response.accepted.length) {
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
},
|
||||
"prettier": "@kan/prettier-config",
|
||||
"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 "./generateSlug";
|
||||
export * from "./subscriptions";
|
||||
export * from "./email";
|
||||
|
||||
@@ -71,4 +71,13 @@ export const updateSubscriptionSeats = async (
|
||||
return updatedSubscription;
|
||||
};
|
||||
|
||||
export const getCancellationDetails = async (
|
||||
stripeSubscriptionId: string,
|
||||
): Promise<Stripe.Subscription.CancellationDetails | null> => {
|
||||
const stripe = createStripeClient();
|
||||
const stripeSubscription =
|
||||
await stripe.subscriptions.retrieve(stripeSubscriptionId);
|
||||
return stripeSubscription.cancellation_details ?? null;
|
||||
};
|
||||
|
||||
export { createStripeClient };
|
||||
|
||||
84
pnpm-lock.yaml
generated
84
pnpm-lock.yaml
generated
@@ -115,6 +115,9 @@ importers:
|
||||
'@lingui/react':
|
||||
specifier: ^5.3.2
|
||||
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':
|
||||
specifier: ^0.11.1
|
||||
version: 0.11.1(typescript@5.9.2)(zod@3.25.76)
|
||||
@@ -166,6 +169,9 @@ importers:
|
||||
geist:
|
||||
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))
|
||||
jose:
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
next:
|
||||
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)
|
||||
@@ -223,6 +229,9 @@ importers:
|
||||
tippy.js:
|
||||
specifier: ^6.3.7
|
||||
version: 6.3.7
|
||||
tiptap-markdown:
|
||||
specifier: ^0.8
|
||||
version: 0.8.10(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))
|
||||
zod:
|
||||
specifier: 'catalog:'
|
||||
version: 3.25.76
|
||||
@@ -290,6 +299,12 @@ importers:
|
||||
|
||||
packages/api:
|
||||
dependencies:
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: ^3.802.0
|
||||
version: 3.879.0
|
||||
'@aws-sdk/s3-request-presigner':
|
||||
specifier: ^3.812.0
|
||||
version: 3.879.0
|
||||
'@kan/auth':
|
||||
specifier: workspace:*
|
||||
version: link:../auth
|
||||
@@ -431,9 +446,9 @@ importers:
|
||||
|
||||
packages/email:
|
||||
dependencies:
|
||||
'@plunk/node':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@novu/api':
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0
|
||||
'@react-email/components':
|
||||
specifier: ^0.0.25
|
||||
version: 0.0.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -468,9 +483,15 @@ importers:
|
||||
|
||||
packages/shared:
|
||||
dependencies:
|
||||
jose:
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
nanoid:
|
||||
specifier: ^5.0.9
|
||||
version: 5.1.5
|
||||
next-runtime-env:
|
||||
specifier: ^1.7.2
|
||||
version: 1.8.0
|
||||
devDependencies:
|
||||
'@kan/eslint-config':
|
||||
specifier: workspace:*
|
||||
@@ -2674,6 +2695,9 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@novu/api@3.11.0':
|
||||
resolution: {integrity: sha512-8u0mB5VThL7MhdxoN0UoA4CS9eu2k3Xa6iulauMhCHENuJNxUNuirJWq5t8jDoH4bFTQTfMN0VRkC0qodKR2qA==}
|
||||
|
||||
'@octokit/auth-token@3.0.4':
|
||||
resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -2753,9 +2777,6 @@ packages:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@plunk/node@3.0.3':
|
||||
resolution: {integrity: sha512-f3NT418/EdLjebQ4arAwR64cnz7d5H7i1CeKh8+B0cVBrwzHvF+Q+QkhutF+FAp49yuI/BYc71qQH+RFc2fxxQ==}
|
||||
|
||||
'@popperjs/core@2.11.8':
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
|
||||
@@ -3575,9 +3596,15 @@ packages:
|
||||
'@types/katex@0.16.7':
|
||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||
|
||||
'@types/linkify-it@3.0.5':
|
||||
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
|
||||
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||
|
||||
@@ -3587,6 +3614,9 @@ packages:
|
||||
'@types/mdast@4.0.4':
|
||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||
|
||||
'@types/mdurl@1.0.5':
|
||||
resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==}
|
||||
|
||||
'@types/mdurl@2.0.0':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
|
||||
@@ -5536,6 +5566,9 @@ packages:
|
||||
jose@5.10.0:
|
||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||
|
||||
jose@6.1.2:
|
||||
resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5730,6 +5763,9 @@ packages:
|
||||
make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
|
||||
markdown-it-task-lists@2.1.1:
|
||||
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
@@ -7411,6 +7447,11 @@ packages:
|
||||
tippy.js@6.3.7:
|
||||
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
|
||||
|
||||
tiptap-markdown@0.8.10:
|
||||
resolution: {integrity: sha512-iDVkR2BjAqkTDtFX0h94yVvE2AihCXlF0Q7RIXSJPRSR5I0PA1TMuAg6FHFpmqTn4tPxJ0by0CK7PUMlnFLGEQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.0.3
|
||||
|
||||
title-case@2.1.1:
|
||||
resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==}
|
||||
|
||||
@@ -10200,6 +10241,10 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.19.1
|
||||
|
||||
'@novu/api@3.11.0':
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
'@octokit/auth-token@3.0.4': {}
|
||||
|
||||
'@octokit/core@4.2.4':
|
||||
@@ -10319,10 +10364,6 @@ snapshots:
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@plunk/node@3.0.3':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
|
||||
'@posthog/core@1.0.2': {}
|
||||
@@ -11321,8 +11362,15 @@ snapshots:
|
||||
|
||||
'@types/katex@0.16.7': {}
|
||||
|
||||
'@types/linkify-it@3.0.5': {}
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
dependencies:
|
||||
'@types/linkify-it': 3.0.5
|
||||
'@types/mdurl': 1.0.5
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
@@ -11336,6 +11384,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/mdurl@1.0.5': {}
|
||||
|
||||
'@types/mdurl@2.0.0': {}
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
@@ -13614,6 +13664,8 @@ snapshots:
|
||||
|
||||
jose@5.10.0: {}
|
||||
|
||||
jose@6.1.2: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
js-beautify@1.15.4:
|
||||
@@ -13777,6 +13829,8 @@ snapshots:
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
markdown-it-task-lists@2.1.1: {}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
@@ -16085,6 +16139,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
|
||||
tiptap-markdown@0.8.10(@tiptap/core@2.26.1(@tiptap/pm@2.26.1)):
|
||||
dependencies:
|
||||
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
|
||||
'@types/markdown-it': 13.0.9
|
||||
markdown-it: 14.1.0
|
||||
markdown-it-task-lists: 2.1.1
|
||||
prosemirror-markdown: 1.13.2
|
||||
|
||||
title-case@2.1.1:
|
||||
dependencies:
|
||||
no-case: 2.3.2
|
||||
@@ -16466,7 +16528,7 @@ snapshots:
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.15.0)
|
||||
browserslist: 4.25.4
|
||||
browserslist: 4.27.0
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.3
|
||||
es-module-lexer: 1.7.0
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
"NEXT_PUBLIC_KAN_ENV",
|
||||
"NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY",
|
||||
"STRIPE_SECRET_KEY",
|
||||
"DISCORD_WEBHOOK_URL",
|
||||
"STRIPE_WEBHOOK_SECRET",
|
||||
"STRIPE_WEBHOOK_SECRET_LEGACY",
|
||||
"STRIPE_PRO_PLAN_MONTHLY_PRICE_ID",
|
||||
@@ -108,6 +109,7 @@
|
||||
"NEXT_PUBLIC_STORAGE_DOMAIN",
|
||||
"NEXT_PUBLIC_STORAGE_URL",
|
||||
"NEXT_PUBLIC_AVATAR_BUCKET_NAME",
|
||||
"NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME",
|
||||
"NEXT_PUBLIC_ALLOW_CREDENTIALS",
|
||||
"NEXT_PUBLIC_DISABLE_SIGN_UP",
|
||||
"NEXT_PUBLIC_USE_STANDALONE_OUTPUT",
|
||||
@@ -119,8 +121,8 @@
|
||||
"PORT",
|
||||
"BETTER_AUTH_SECRET",
|
||||
"BETTER_AUTH_TRUSTED_ORIGINS",
|
||||
"PLUNK_API_KEY",
|
||||
"PLUNK_API_URL"
|
||||
"NOVU_API_KEY",
|
||||
"EMAIL_UNSUBSCRIBE_SECRET"
|
||||
],
|
||||
"globalPassThroughEnv": [
|
||||
"NODE_ENV",
|
||||
|
||||
Reference in New Issue
Block a user