Compare commits

...

38 Commits

Author SHA1 Message Date
Henry
a31885e265 feat: add returnUrl params 2026-04-10 21:43:48 +01:00
Henry
5c5d208fc4 feat: set slug on workspace name change 2026-04-10 21:26:56 +01:00
Henry
cb76926041 feat: create workspace on successful activation 2026-04-10 15:28:27 +01:00
Henry
6dfee580f7 refactor: improve webhook logs 2026-04-05 23:09:01 +01:00
Henry
e20104a292 chore: update types 2026-04-05 23:01:21 +01:00
Henry
dbf53717db feat: create worspace on checkout.session.completed 2026-04-05 23:00:12 +01:00
Henry
0dc5092ebc feat: add logging to next api routes 2026-04-05 22:33:06 +01:00
Henry
e81f5d7fe6 feat: allow setting description when creating a workspace 2026-04-03 00:03:12 +01:00
Henry
f4bb7f8443 feat: add team to workspace plans 2026-04-03 00:02:39 +01:00
Henry
ac2d675934 refactor: tweak onboarding steps UX 2026-04-03 00:00:18 +01:00
Henry
edc9e1254e fix: validate/convert slug input 2026-04-02 13:06:22 +01:00
Henry
a713d09c46 feat: tweak padding in boards mockup 2026-04-01 23:46:35 +01:00
Henry
385bc0de61 refactor: optimize board rendering in workspace details 2026-04-01 23:41:13 +01:00
Henry
1e93d85318 feat: add workspace details step 2026-04-01 23:30:00 +01:00
Henry
abaf2badb0 feat: setup onboarding and select plan page 2026-04-01 21:26:12 +01:00
hjball
603cc46b7d chore: compile translations 2026-04-01 13:35:04 +00:00
hjball
e9d338384a chore: update translations 2026-04-01 13:34:59 +00:00
Nick Meinhold
8a2a9ecef5 fix: avatar upload display and oversized crop on HiDPI screens (#442)
Two related avatar bugs:

1. getAvatarUrl() returned empty string for S3 keys, so uploaded
   avatars never displayed. Now constructs the full URL using
   NEXT_PUBLIC_STORAGE_URL and NEXT_PUBLIC_AVATAR_BUCKET_NAME,
   with support for both path-style (MinIO) and virtual-hosted
   (Tigris/AWS S3) URLs.

2. Avatar crop scaled canvas by devicePixelRatio (4x pixels on
   Retina), producing blobs that exceeded the 2MB upload limit
   even for small source images. Now caps output at 512x512 and
   uses quality=0.85 for toBlob().

Closes #440, closes #441

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-01 14:33:11 +01:00
Charity
3a44957662 feat: add card context menu and duplication functionality (#381)
* feat: add card context menu and duplication functionality

* Implemented a context menu for cards allowing actions such as copying links, duplicating cards, and managing members, labels, and due dates
* Added modals for card duplication and context actions.
* Updated API with a new endpoint for duplicating cards, including options for copying labels, members, and checklists

* refactor: remove cardPublicId from context menu and related components

* Removed cardPublicId prop from CardContextMenu and CardContextMembersModal for cleaner context handling
* Updated scrollbar styling

* feat: add delete card functionality to context menu
2026-04-01 14:32:56 +01:00
Henry
20e6c99e32 feat: enhance logging with request input (#450) 2026-03-23 22:31:02 +00:00
Henry
c0aae5449f fix: prevent invalid calls to fetch label by publicId (#449)
* fix: prevent invalid calls to fetch label by publicId

* fix: ensure query only runs when isEdit is true
2026-03-23 21:40:35 +00:00
Henry
596f346181 fix: increase maximum length for boardSlug from 24 to 60 characters (#448) 2026-03-23 16:30:29 +00:00
Henry
7b59e2ac07 feat(cloud): log to docker container 2026-03-22 07:21:30 +00:00
Henry
2a9b1fd2b5 refactor: replace @axiomhq/pino with @axiomhq/js 2026-03-20 13:56:52 +00:00
Henry
a2d4314e14 fix: update Dockerfile to include pino transport packages 2026-03-20 13:26:27 +00:00
Henry
f94e4d6da2 fix: include pino transports in outputFileTracingIncludes 2026-03-20 12:04:46 +00:00
Henry
97f4defc0e fix: add pino to serverExternalPackages 2026-03-20 11:46:08 +00:00
Henry
dd29dc89fd fix: remove broken dep 2026-03-19 21:33:44 +00:00
Henry
486cdf8313 feat(cloud): setup log drain (#447) 2026-03-19 21:16:47 +00:00
Henry
67cf523ee8 fix: API key authentication and REST handler errors (#446) 2026-03-19 12:20:19 +00:00
hjball
0b45ceff7f chore: update translations 2026-03-16 14:32:37 +00:00
Henry
76b2c58461 fix: security improvements to checklist input (#444)
* feat: sanitize input for checklists

* feat: add basic editor

* feat: use plain text editor for checklist items

* feat: remove key from user response
2026-03-16 14:31:13 +00:00
Henry
b8d1fe230d fix: prevent empty string from throwing error 2026-03-13 23:08:46 +00:00
hjball
bd652ba6c9 chore: update translations 2026-03-13 22:59:04 +00:00
Henry
1fb56aafd0 feat: add logger package to improve observability (#437)
* feat: add logger package to improve observability

* chore: add LOG_LEVEL to docker compose and readme
2026-03-13 22:57:47 +00:00
hjball
91deaca6ec chore: compile translations 2026-03-13 22:45:15 +00:00
hjball
ab81db0f56 chore: update translations 2026-03-13 22:45:11 +00:00
Andy Clemenko
29080fa851 Fontsize PR for Issue #429 (#430)
* adding font size

* removing trash

* moving font menu

* chore: revert port change in compose

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
2026-03-13 22:43:50 +00:00
103 changed files with 9720 additions and 1047 deletions

View File

@@ -51,6 +51,9 @@ TRELLO_APP_SECRET=
# If not provided, rate limiting will use in-memory storage
REDIS_URL= # e.g. redis://default:your_password@your_host:6379
# Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod)
LOG_LEVEL=
# OAuth providers (optional)
BETTER_AUTH_TRUSTED_ORIGINS=
# Optional: Restrict OIDC/Social sign-ins to specific email domains (comma-separated)

2
.gitignore vendored
View File

@@ -54,3 +54,5 @@ i18n.cache
# pgdata
/apps/web/pgdata
.claude

View File

@@ -185,9 +185,17 @@ Use `assertUserInWorkspace` helper for workspace checks.
- Use TRPCError with appropriate codes (UNAUTHORIZED, NOT_FOUND, etc.)
- Provide user-friendly error messages
- Log errors appropriately
- Log errors appropriately using the `@kan/logger` package
- Show popup notifications for user-facing errors
### Logging
- Import from `@kan/logger`: `import { createLogger } from "@kan/logger"`
- Create a module-scoped logger: `const logger = createLogger("module-name")`
- Log level is controlled by `LOG_LEVEL` env var (debug, info, warn, error)
- Defaults to `debug` in development, `info` in production
- Never use `console.log` — always use the logger
## Common Patterns
### Creating a Card
@@ -219,6 +227,16 @@ Use `assertUserInWorkspace` helper for workspace checks.
5. **Frontend**: Add UI components in `apps/web/src/`
6. **i18n**: Add translations for new strings
## Adding a New Environment Variable
Update all of the following:
1. `.env.example` — add the variable with an empty value and a comment explaining it
2. `turbo.json` — add to `globalEnv` (or `globalPassThroughEnv` for CI/platform vars)
3. `docker-compose.yml` — add to the `web` service `environment` section
4. `cloud/docker-compose.yml` — add to the `web` service `environment` section
5. `README.md` — add a row to the Environment Variables table
## Database Changes
- Always create migrations (never modify existing migrations)

View File

@@ -213,6 +213,7 @@ pnpm dev
| `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` |
| `KAN_ADMIN_API_KEY` | Admin API key for stats and admin endpoints | For admin/monitoring | `your-secret-admin-key` |
| `LOG_LEVEL` | Log verbosity level (debug, info, warn, error) | No (defaults to debug in dev, info in prod) | `info` |
See `.env.example` for a complete list of supported environment variables.

View File

@@ -11,10 +11,10 @@ https://kan.bn/api/v1
## Authentication
Most endpoints require authentication using your API key. You can create one in the [settings page](https://kan.bn/settings) of your account. Include this key in the `x-api-key` header of each request.
Most endpoints require authentication using your API key. You can create one in the [settings page](https://kan.bn/settings) of your account. Include this key as a Bearer token in the `Authorization` header of each request.
```
'x-api-key': kan_123456789
'Authorization': 'Bearer kan_123456789'
```
## Response codes

View File

@@ -103,6 +103,9 @@ checksums:
tMYIu7/message: 8b1f0bb96a905646ecfad1cfdfa42168
tMYIu7/origin/0/0: d77eb1f90f2fa86a55181629d88d237a
tMYIu7/translation: 8b1f0bb96a905646ecfad1cfdfa42168
UGCIzB/translation: f7b6699c16726afc48f1e1d1339b2e7e
UGCIzB/message: f7b6699c16726afc48f1e1d1339b2e7e
UGCIzB/origin/0/0: 0eeeb7e68164ff118040918f1ef00a7a
B3xxao/message: 9d80becc4fe65a74e0004e4fd290735d
B3xxao/origin/0/0: 6812b79a8fdc3527586b523d4c137340
B3xxao/translation: 9d80becc4fe65a74e0004e4fd290735d
@@ -516,23 +519,27 @@ checksums:
dEgA5A/message: 2e2a849c2223911717de8caa2c71bade
dEgA5A/origin/0/0: cf5db0532c4f1cf49cebe78926c03385
dEgA5A/origin/1/0: a50c4fbde8da6e0a77c4e0aef0b44dea
dEgA5A/origin/2/0: 522726bb62051656dc0d165185d6b451
dEgA5A/origin/3/0: b2059fa4b8b99cc10440da26475b234c
dEgA5A/origin/4/0: 2a382c1cdf010f01a8e95c3e05f6df53
dEgA5A/origin/5/0: d55f532bb5ae247e3a018978be0da463
dEgA5A/origin/6/0: 4b4f55c033add8db69178563832227c6
dEgA5A/origin/7/0: 68450df287fb46ea629e818732c1291a
dEgA5A/origin/8/0: 03c00909b41be01231cd3e843c8ee44e
dEgA5A/origin/9/0: 0627e0040ca4939c9a57349e98c51797
dEgA5A/origin/10/0: 15792234f408027822ee71ccddde6f9b
dEgA5A/origin/11/0: 5c60161e173f5eae9276d8e4c70b4ebb
dEgA5A/origin/12/0: a1fde3fcce608870830f168f027727ff
dEgA5A/origin/13/0: 83911e3eacbad4583e2b1647a784d154
dEgA5A/origin/14/0: dbad0c8d7863cb41f5495264f82e7081
dEgA5A/origin/2/0: 97bd206a05414e2a571020d64e166093
dEgA5A/origin/3/0: 522726bb62051656dc0d165185d6b451
dEgA5A/origin/4/0: b2059fa4b8b99cc10440da26475b234c
dEgA5A/origin/5/0: 2a382c1cdf010f01a8e95c3e05f6df53
dEgA5A/origin/6/0: d55f532bb5ae247e3a018978be0da463
dEgA5A/origin/7/0: 4b4f55c033add8db69178563832227c6
dEgA5A/origin/8/0: 68450df287fb46ea629e818732c1291a
dEgA5A/origin/9/0: 03c00909b41be01231cd3e843c8ee44e
dEgA5A/origin/10/0: 0627e0040ca4939c9a57349e98c51797
dEgA5A/origin/11/0: 15792234f408027822ee71ccddde6f9b
dEgA5A/origin/12/0: 5c60161e173f5eae9276d8e4c70b4ebb
dEgA5A/origin/13/0: a1fde3fcce608870830f168f027727ff
dEgA5A/origin/14/0: 83911e3eacbad4583e2b1647a784d154
dEgA5A/origin/15/0: dbad0c8d7863cb41f5495264f82e7081
dEgA5A/translation: 2e2a849c2223911717de8caa2c71bade
kryGs%2B/message: bba0beaced7ea954ceb980f2b022ffee
kryGs%2B/origin/0/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
kryGs%2B/translation: bba0beaced7ea954ceb980f2b022ffee
sU2uWc/translation: 3fe52244676faf64e5266b6713d01e07
sU2uWc/message: 3fe52244676faf64e5266b6713d01e07
sU2uWc/origin/0/0: 97bd206a05414e2a571020d64e166093
fEY2vP/message: 91509e2f92b0b3b11330b6983139fdbf
fEY2vP/origin/0/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
fEY2vP/origin/1/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
@@ -541,8 +548,9 @@ checksums:
ZH7TVS/origin/0/0: 984122104bcc6d8fa6d4c8a35e7751d2
ZH7TVS/translation: 7c34f59f4005e6cb3a6ff546ea0b96e3
rQXTmd/message: 9387a4702b302f23f2f7c20b73a2b3dd
rQXTmd/origin/0/0: 1b05beba6bdd908015fa8e798717f024
rQXTmd/origin/1/0: 9c8b544f5db130cd7c5eda9064ba3957
rQXTmd/origin/0/0: fc0c18a095a68277a0f90a115942683b
rQXTmd/origin/1/0: 1b05beba6bdd908015fa8e798717f024
rQXTmd/origin/2/0: 9c8b544f5db130cd7c5eda9064ba3957
rQXTmd/translation: 9387a4702b302f23f2f7c20b73a2b3dd
VhMDMg/message: a552fc5c4189ebc3e2e6018edda7d18f
VhMDMg/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
@@ -550,6 +558,9 @@ checksums:
VhMDMg/origin/2/0: 0627e0040ca4939c9a57349e98c51797
VhMDMg/origin/3/0: 0627e0040ca4939c9a57349e98c51797
VhMDMg/translation: a552fc5c4189ebc3e2e6018edda7d18f
fzGyWs/message: 25dbbca7a83f6af119179e7a56c953d4
fzGyWs/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
fzGyWs/translation: 25dbbca7a83f6af119179e7a56c953d4
yD3ZSw/message: 293d49fc3c75e9c425b64bd7126e6b46
yD3ZSw/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
yD3ZSw/translation: 293d49fc3c75e9c425b64bd7126e6b46
@@ -696,6 +707,18 @@ checksums:
F0YmUY/message: 9def232c9163d109f0e9093684cfe75f
F0YmUY/origin/0/0: 1b05beba6bdd908015fa8e798717f024
F0YmUY/translation: 9def232c9163d109f0e9093684cfe75f
0utuU6/translation: 9d7e453b24dfaf217352aeadc262074d
0utuU6/message: 9d7e453b24dfaf217352aeadc262074d
0utuU6/origin/0/0: 97bd206a05414e2a571020d64e166093
fDQ7hA/translation: 1d24ecbad9f2f0081ab51c7366e40506
fDQ7hA/message: 1d24ecbad9f2f0081ab51c7366e40506
fDQ7hA/origin/0/0: 97bd206a05414e2a571020d64e166093
1bJKhj/translation: f61803ac8da8253e4ba35f92b3af1101
1bJKhj/message: f61803ac8da8253e4ba35f92b3af1101
1bJKhj/origin/0/0: 0eeeb7e68164ff118040918f1ef00a7a
uvH2xS/translation: ecc2617544a6d385d18678577fa112fd
uvH2xS/message: ecc2617544a6d385d18678577fa112fd
uvH2xS/origin/0/0: 97bd206a05414e2a571020d64e166093
9PaIxv/message: da95932e7a1465a5d21aa3b855a46dc2
9PaIxv/origin/0/0: c0b59968b2b234167c488a05cc38710b
9PaIxv/translation: da95932e7a1465a5d21aa3b855a46dc2
@@ -835,7 +858,8 @@ checksums:
tmpW%2Bw/origin/0/0: 0ad2af981579a62b084b3d41462c5546
tmpW%2Bw/translation: e915cd160651ad4b61a67e8da0737c23
nabda1/message: 18811ccf76fbb7a2772d648f7c5f0ea7
nabda1/origin/0/0: 1b05beba6bdd908015fa8e798717f024
nabda1/origin/0/0: 0eeeb7e68164ff118040918f1ef00a7a
nabda1/origin/1/0: 1b05beba6bdd908015fa8e798717f024
nabda1/translation: 18811ccf76fbb7a2772d648f7c5f0ea7
njJFtc/message: a3dae0c9ddbc73677aad9812e3dffd69
njJFtc/origin/0/0: b2059fa4b8b99cc10440da26475b234c
@@ -916,12 +940,15 @@ checksums:
vUr5JT/origin/0/0: 4a1be3707d14c696a16f453d132fb248
vUr5JT/translation: 6ae8282ab8b4a01b4ce87b119e13714b
DPfwMq/message: ffd408fa29d5bc9039ef8ea1b9b699bb
DPfwMq/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/2/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/0/0: 81b173297e9c022d9ebde5f7018c95f9
DPfwMq/origin/1/0: 120e67abaa784f3adb192e7519919a81
DPfwMq/origin/2/0: fc68b5394b9b466d5d67df855060544d
DPfwMq/origin/3/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/4/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/5/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/6/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/7/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/origin/8/0: eb63312c63f6c8d2a5b6520c89012123
DPfwMq/translation: ffd408fa29d5bc9039ef8ea1b9b699bb
R3FsF4/message: 142b391cca3e05f2af9617092f7358c5
R3FsF4/origin/0/0: 3a89a46ac678e34dc8eb687b07a4ad4b
@@ -943,6 +970,16 @@ checksums:
zxKs%2By/message: 0b6c8ad7aba0873b7e212d5f1949ca97
zxKs%2By/origin/0/0: db1e755dc7d73e037f425111bf7c30a6
zxKs%2By/translation: 0b6c8ad7aba0873b7e212d5f1949ca97
euc6Ns/translation: 27756566785c2b8463e21582c4bb619b
euc6Ns/message: 27756566785c2b8463e21582c4bb619b
euc6Ns/origin/0/0: 97bd206a05414e2a571020d64e166093
BTDPLo/translation: f6a33afcf5ff1489551c94867e083dca
BTDPLo/message: f6a33afcf5ff1489551c94867e083dca
BTDPLo/origin/0/0: 97bd206a05414e2a571020d64e166093
BTDPLo/origin/1/0: 0eeeb7e68164ff118040918f1ef00a7a
06EQqT/translation: e9f56489fb395a890ae2557200c29a36
06EQqT/message: e9f56489fb395a890ae2557200c29a36
06EQqT/origin/0/0: 97bd206a05414e2a571020d64e166093
ePK91l/message: eee7f39ff90b18852afc1671f21fbaa9
ePK91l/origin/0/0: b697a20a9536be4dd8ecd143461eafc7
ePK91l/origin/1/0: dc1a240d2d0cccb102c67ca311d4e457
@@ -989,6 +1026,9 @@ checksums:
IqjpYe/message: 81d41cf573a7109c376d30d905beb596
IqjpYe/origin/0/0: 5f6c4a0c9be5098099167871ae1564af
IqjpYe/translation: 81d41cf573a7109c376d30d905beb596
bFREhu/translation: b2cc9a311fc28d1594252f697c7f5342
bFREhu/message: b2cc9a311fc28d1594252f697c7f5342
bFREhu/origin/0/0: 97bd206a05414e2a571020d64e166093
0%2BewPp/message: 785fe23c0eef0a5b60b5b2a88151de31
0%2BewPp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
0%2BewPp/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
@@ -1167,6 +1207,9 @@ checksums:
l31EoQ/message: b4c05c9174637f4da2a41ead0d65bc01
l31EoQ/origin/0/0: 4980d8e27a628e7cd9a70b839d844083
l31EoQ/translation: b4c05c9174637f4da2a41ead0d65bc01
q3il6U/message: 79a4fffd70bfbedb6174ea4c767f4ab9
q3il6U/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
q3il6U/translation: 79a4fffd70bfbedb6174ea4c767f4ab9
"%2B3TYXa/message": 9fa1c942feb40d6350322b71561df789
"%2B3TYXa/origin/0/0": 4980d8e27a628e7cd9a70b839d844083
"%2B3TYXa/translation": 9fa1c942feb40d6350322b71561df789
@@ -1420,9 +1463,10 @@ checksums:
h7MgpO/origin/0/0: feacdaf2791aaddec128f463ffa2e807
h7MgpO/translation: ef00d7494b69def6841620bd6554d040
h8DugX/message: 6f15627a90002323eac018274b6922d6
h8DugX/origin/0/0: db1e755dc7d73e037f425111bf7c30a6
h8DugX/origin/1/0: 984122104bcc6d8fa6d4c8a35e7751d2
h8DugX/origin/2/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
h8DugX/origin/0/0: 120e67abaa784f3adb192e7519919a81
h8DugX/origin/1/0: db1e755dc7d73e037f425111bf7c30a6
h8DugX/origin/2/0: 984122104bcc6d8fa6d4c8a35e7751d2
h8DugX/origin/3/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
h8DugX/translation: 6f15627a90002323eac018274b6922d6
AZI%2Fwb/message: e1ceda0c6cc32fb04cb9423582ad8fe4
AZI%2Fwb/origin/0/0: c0b59968b2b234167c488a05cc38710b
@@ -1433,6 +1477,9 @@ checksums:
vXIe7J/message: 277fd1a41cc237a437cd1d5e4a80463b
vXIe7J/origin/0/0: 627b72cd19a7154ae1c28cbd7ca0c730
vXIe7J/translation: 277fd1a41cc237a437cd1d5e4a80463b
k7rCa%2F/message: 890b7851af05f97ac53644e4154c13b4
k7rCa%2F/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
k7rCa%2F/translation: 890b7851af05f97ac53644e4154c13b4
8Is1ih/message: 624592ba2ecf647a3fe60d493c1ee71a
8Is1ih/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
8Is1ih/origin/1/0: fadd5a5b6963ec5f261072f7e8d2140b
@@ -1464,12 +1511,14 @@ checksums:
TCt%2F8K/translation: ffd8033392a01d7b33f13afa51bccb9a
edWbV6/message: 4a4805700b31ec65bcb3ca962bccfd1b
edWbV6/origin/0/0: 85d0a3237fa9329fba3f0278a2cd7284
edWbV6/origin/1/0: 1b05beba6bdd908015fa8e798717f024
edWbV6/origin/2/0: 9c8b544f5db130cd7c5eda9064ba3957
edWbV6/origin/3/0: faa8a2576972b4eb31565d8ea6098f43
edWbV6/origin/1/0: fc0c18a095a68277a0f90a115942683b
edWbV6/origin/2/0: 1b05beba6bdd908015fa8e798717f024
edWbV6/origin/3/0: 9c8b544f5db130cd7c5eda9064ba3957
edWbV6/origin/4/0: faa8a2576972b4eb31565d8ea6098f43
edWbV6/translation: 4a4805700b31ec65bcb3ca962bccfd1b
2BBAbc/message: 94f13e7ef909a4de9db7abaa1f9f0b61
2BBAbc/origin/0/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
2BBAbc/origin/0/0: 97bd206a05414e2a571020d64e166093
2BBAbc/origin/1/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
2BBAbc/translation: 94f13e7ef909a4de9db7abaa1f9f0b61
zQX5Dj/message: e925e2e6ccaf0eb4064a888aaea8d3c2
zQX5Dj/origin/0/0: 5e445d66b146e0ca392faa35098701c4
@@ -1507,6 +1556,10 @@ checksums:
DbZgsJ/message: 7a1c63b75cea690b3fda6c3f203e31ff
DbZgsJ/origin/0/0: 0ad2af981579a62b084b3d41462c5546
DbZgsJ/translation: 7a1c63b75cea690b3fda6c3f203e31ff
hB02vO/translation: c13aa2a155ab4aff1c8fed6650a94e1a
hB02vO/message: c13aa2a155ab4aff1c8fed6650a94e1a
hB02vO/origin/0/0: fc68b5394b9b466d5d67df855060544d
hB02vO/origin/1/0: 0eeeb7e68164ff118040918f1ef00a7a
IDvohQ/message: 35d0822f65971b97774b962561b02649
IDvohQ/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
IDvohQ/translation: 35d0822f65971b97774b962561b02649
@@ -1514,6 +1567,9 @@ checksums:
jl3aEJ/placeholders/0/0: 06f23520ebabfffed4999b5bcf61c9cd
jl3aEJ/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
jl3aEJ/translation: 4c38799ff25321ea25017bf4cd8e2e4f
agPptk/message: 43983f2178fb28aacaef6c5f41282c4a
agPptk/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
agPptk/translation: 43983f2178fb28aacaef6c5f41282c4a
W%2FElkg/message: 1f527cd6d1ed602930bcaa303f503b51
W%2FElkg/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
W%2FElkg/translation: 1f527cd6d1ed602930bcaa303f503b51
@@ -1537,9 +1593,9 @@ checksums:
"%2FbZzdR/message": 40ea106c56d79634e058e087ad322025
"%2FbZzdR/origin/0/0": c0b59968b2b234167c488a05cc38710b
"%2FbZzdR/translation": 40ea106c56d79634e058e087ad322025
hty0d5/translation: b4b1c8396e445e8b7956c747da2cf601
hty0d5/message: b4b1c8396e445e8b7956c747da2cf601
hty0d5/origin/0/0: 04c86ea96f180745739d59e822da4026
hty0d5/translation: b4b1c8396e445e8b7956c747da2cf601
"%2B8Nek%2F/message": 818f1192e32bb855597f930d3e78806e
"%2B8Nek%2F/origin/0/0": 9737a9cef0646ff4bbd942b1924b9a2c
"%2B8Nek%2F/origin/1/0": 82a5cf8d69f098641eb357bf102cd504
@@ -1547,6 +1603,12 @@ checksums:
vneRvS/message: e228107df80015377112d41f4f155cb3
vneRvS/origin/0/0: 8628ddb6f4ded25c0f17e2cbda8207d3
vneRvS/translation: e228107df80015377112d41f4f155cb3
51UCsN/translation: ce14fde5e972c2b5276c25ddded817c4
51UCsN/message: ce14fde5e972c2b5276c25ddded817c4
51UCsN/origin/0/0: 0eeeb7e68164ff118040918f1ef00a7a
J4%2BOTA/translation: 6ad27e803483208b5313660b7b5791d3
J4%2BOTA/message: 6ad27e803483208b5313660b7b5791d3
J4%2BOTA/origin/0/0: fd0e66cef8d46e96bcf3143a12f9b6db
BOqTi5/message: 11edf0427766c26db541d46379dc3c16
BOqTi5/placeholders/0/0: 9db68a4b386b9fa1fd1d5797c32be100
BOqTi5/placeholders/1/0: b837a9e8bd06b58be95699b3ee663f43
@@ -1861,10 +1923,16 @@ checksums:
PZCqeW/origin/1/0: 8628ddb6f4ded25c0f17e2cbda8207d3
PZCqeW/translation: 325dea6dd0348a27a6818db2c1340c98
fuwKpE/message: 346a18a54564881c888b95873d03df44
fuwKpE/origin/0/0: 1b05beba6bdd908015fa8e798717f024
fuwKpE/origin/1/0: 9c8b544f5db130cd7c5eda9064ba3957
fuwKpE/origin/2/0: faa8a2576972b4eb31565d8ea6098f43
fuwKpE/origin/0/0: 97bd206a05414e2a571020d64e166093
fuwKpE/origin/1/0: fd0e66cef8d46e96bcf3143a12f9b6db
fuwKpE/origin/2/0: fc0c18a095a68277a0f90a115942683b
fuwKpE/origin/3/0: 1b05beba6bdd908015fa8e798717f024
fuwKpE/origin/4/0: 9c8b544f5db130cd7c5eda9064ba3957
fuwKpE/origin/5/0: faa8a2576972b4eb31565d8ea6098f43
fuwKpE/translation: 346a18a54564881c888b95873d03df44
nYNMZZ/translation: d5a8a1b38eaf02d02cd6ac2211b42a92
nYNMZZ/message: d5a8a1b38eaf02d02cd6ac2211b42a92
nYNMZZ/origin/0/0: 97bd206a05414e2a571020d64e166093
aHCEmh/message: ce27f1aeacccc542a174c4b2bce022b0
aHCEmh/origin/0/0: 98e2302d0d8d12ce65c4140b87450673
aHCEmh/origin/1/0: 634473ef965836871efe40875a36b6ab
@@ -2013,9 +2081,9 @@ checksums:
YEOVrT/message: 86b76024524fc585b2c3950126ef6f62
YEOVrT/origin/0/0: fadd5a5b6963ec5f261072f7e8d2140b
YEOVrT/translation: 86b76024524fc585b2c3950126ef6f62
"%2B5kO8P/translation": 77b65adf8ae2b69eaed51b6104ded65e
"%2B5kO8P/message": 77b65adf8ae2b69eaed51b6104ded65e
"%2B5kO8P/origin/0/0": 04c86ea96f180745739d59e822da4026
"%2B5kO8P/translation": 77b65adf8ae2b69eaed51b6104ded65e
tfDRzk/message: f7a2929f33bc420195e59ac5a8bcd454
tfDRzk/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
tfDRzk/origin/1/0: b2059fa4b8b99cc10440da26475b234c
@@ -2045,6 +2113,9 @@ checksums:
K3iLeO/message: e3bf6f95419169f460fb6918a0afc512
K3iLeO/origin/0/0: 5e2ed4920ffd2f535ece848ff44900bd
K3iLeO/translation: e3bf6f95419169f460fb6918a0afc512
zYRVNp/translation: 8b36455dd5b43e56d2ab06e522c3bacc
zYRVNp/message: 8b36455dd5b43e56d2ab06e522c3bacc
zYRVNp/origin/0/0: 97bd206a05414e2a571020d64e166093
wgNoIs/message: eedc7cdb02de467c15dc418a066a77f2
wgNoIs/origin/0/0: 3c57a918258af4b44c187f58d203345f
wgNoIs/origin/1/0: 3c57a918258af4b44c187f58d203345f
@@ -2074,7 +2145,9 @@ checksums:
v3YoMA/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
v3YoMA/translation: 3fff865dc00435f82896fc302ea45630
8AbRER/message: 3cb81c4829857556f5d117c9a23f7bcc
8AbRER/origin/0/0: 2201bf679c663a732d7f1b3c3ce3897f
8AbRER/origin/0/0: 81b173297e9c022d9ebde5f7018c95f9
8AbRER/origin/1/0: 0eeeb7e68164ff118040918f1ef00a7a
8AbRER/origin/2/0: 2201bf679c663a732d7f1b3c3ce3897f
8AbRER/translation: 3cb81c4829857556f5d117c9a23f7bcc
Eit43O/message: b5d9a1edd988f455041aee9fa7fb3d63
Eit43O/origin/0/0: 1d6b5f88cdad160a67842f6b65387448
@@ -2139,6 +2212,9 @@ checksums:
zEcnBA/message: 513c3b4d9f0fda60fe079d42dcbd3882
zEcnBA/origin/0/0: c0b59968b2b234167c488a05cc38710b
zEcnBA/translation: 513c3b4d9f0fda60fe079d42dcbd3882
4Revpc/message: 3adb3ae7891c5736776df840cff2360b
4Revpc/origin/0/0: 3de48fb04f7df9bdbe399911cdb3fe86
4Revpc/translation: 3adb3ae7891c5736776df840cff2360b
"%2B%2BrVAm/message": 2227508afb0d38a027e323207e47d4a2
"%2B%2BrVAm/origin/0/0": eb63312c63f6c8d2a5b6520c89012123
"%2B%2BrVAm/translation": 2227508afb0d38a027e323207e47d4a2
@@ -2168,9 +2244,9 @@ checksums:
zzDlyQ/origin/0/0: d96af8a06c2e38a72238bcd16e426859
zzDlyQ/origin/1/0: d96af8a06c2e38a72238bcd16e426859
zzDlyQ/translation: c43827becada6750f7a25890905f38b9
DBC3t5/translation: d21934fb36dd8a82e36352968207d44e
DBC3t5/message: d21934fb36dd8a82e36352968207d44e
DBC3t5/origin/0/0: 04c86ea96f180745739d59e822da4026
DBC3t5/translation: d21934fb36dd8a82e36352968207d44e
uZg2%2Bw/message: 21af8119141afcce5f12da489f34db6a
uZg2%2Bw/origin/0/0: dbad0c8d7863cb41f5495264f82e7081
uZg2%2Bw/translation: 21af8119141afcce5f12da489f34db6a
@@ -2235,6 +2311,9 @@ checksums:
C6gv54/message: 1573c77059bc92df17e661befda8d9b7
C6gv54/origin/0/0: 0ad2af981579a62b084b3d41462c5546
C6gv54/translation: 1573c77059bc92df17e661befda8d9b7
nKBUeF/translation: 07e3ce10820b8ab28643e38b2ed51f64
nKBUeF/message: 07e3ce10820b8ab28643e38b2ed51f64
nKBUeF/origin/0/0: 97bd206a05414e2a571020d64e166093
jlB2RY/message: 67a76bf346ab4b48563269e9d941a64a
jlB2RY/origin/0/0: 0627e0040ca4939c9a57349e98c51797
jlB2RY/translation: 67a76bf346ab4b48563269e9d941a64a
@@ -2307,6 +2386,9 @@ checksums:
MHrjPM/message: 344e64395eaff6822a57d18623853e1a
MHrjPM/origin/0/0: a50c4fbde8da6e0a77c4e0aef0b44dea
MHrjPM/translation: 344e64395eaff6822a57d18623853e1a
wK4OTM/translation: b2a89bfd8e13d77f626e84840602a603
wK4OTM/message: b2a89bfd8e13d77f626e84840602a603
wK4OTM/origin/0/0: 97bd206a05414e2a571020d64e166093
Se%2FUIp/message: d60813ea824f373462471e092d136eed
Se%2FUIp/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
Se%2FUIp/origin/1/0: eb63312c63f6c8d2a5b6520c89012123
@@ -2349,9 +2431,10 @@ checksums:
LeM5RY/origin/0/0: 0dccc39d0debbf3e089cb6d292de762b
LeM5RY/translation: dfeac280858332082ad34d2623a59863
W1ewR0/message: ddcac5d14d48358b043a9487b42eec16
W1ewR0/origin/0/0: 1b05beba6bdd908015fa8e798717f024
W1ewR0/origin/1/0: 9c8b544f5db130cd7c5eda9064ba3957
W1ewR0/origin/2/0: faa8a2576972b4eb31565d8ea6098f43
W1ewR0/origin/0/0: fc0c18a095a68277a0f90a115942683b
W1ewR0/origin/1/0: 1b05beba6bdd908015fa8e798717f024
W1ewR0/origin/2/0: 9c8b544f5db130cd7c5eda9064ba3957
W1ewR0/origin/3/0: faa8a2576972b4eb31565d8ea6098f43
W1ewR0/translation: ddcac5d14d48358b043a9487b42eec16
zl1b97/message: 90112ea12ec6fa42097a6e022ae048a4
zl1b97/origin/0/0: 984122104bcc6d8fa6d4c8a35e7751d2
@@ -2384,6 +2467,12 @@ checksums:
2QGEbi/message: 550198b2c87f06726a843c79c1026ed9
2QGEbi/origin/0/0: 4b4f55c033add8db69178563832227c6
2QGEbi/translation: 550198b2c87f06726a843c79c1026ed9
Oo4E6p/translation: af985cf72c47df76e2f2139a427d9948
Oo4E6p/message: af985cf72c47df76e2f2139a427d9948
Oo4E6p/origin/0/0: 97bd206a05414e2a571020d64e166093
K7k9u3/translation: 1cf9d227042f6a65adba38cb7296830d
K7k9u3/message: 1cf9d227042f6a65adba38cb7296830d
K7k9u3/origin/0/0: fd0e66cef8d46e96bcf3143a12f9b6db
8yFGGF/message: 39025a0c53818438829603d213baef06
8yFGGF/origin/0/0: 68450df287fb46ea629e818732c1291a
8yFGGF/translation: 39025a0c53818438829603d213baef06
@@ -2626,9 +2715,9 @@ checksums:
v1kQyJ/origin/0/0: 77c9f6139024b2069e4fdc5120f69cb5
v1kQyJ/origin/1/0: 566b5982085ae649b2be0c614f666591
v1kQyJ/translation: 0329bc102d3ab3c73410d126e119de12
xLTZVf/translation: ce928c74255344905f5c3f02f44c991e
xLTZVf/message: ce928c74255344905f5c3f02f44c991e
xLTZVf/origin/0/0: 5f6c4a0c9be5098099167871ae1564af
xLTZVf/translation: ce928c74255344905f5c3f02f44c991e
9eF5oV/message: 4928884739ba559e6e4b960e80fe1452
9eF5oV/origin/0/0: 4a1be3707d14c696a16f453d132fb248
9eF5oV/translation: 4928884739ba559e6e4b960e80fe1452

View File

@@ -28,6 +28,7 @@ const config = {
],
},
/** Enables hot reloading for local packages without a build step */
transpilePackages: [
"@kan/api",
@@ -65,6 +66,8 @@ const config = {
},
},
},
serverExternalPackages: ["pino"],
experimental: {
// instrumentationHook: true,
swcPlugins: [["@lingui/swc-plugin", {}]],

View File

@@ -27,6 +27,7 @@
"@kan/api": "workspace:*",
"@kan/auth": "workspace:*",
"@kan/db": "workspace:^",
"@kan/logger": "workspace:^",
"@kan/shared": "workspace:^",
"@lingui/babel-preset-react": "^2.9.2",
"@lingui/conf": "^5.3.2",

View File

@@ -145,7 +145,6 @@ export default function CommandPallette({
value={result}
className="cursor-pointer select-none px-4 py-3 data-[focus]:bg-light-200 hover:bg-light-200 focus:outline-none dark:data-[focus]:bg-dark-200 dark:hover:bg-dark-200"
onClick={() => {
console.log("clicked", url);
void router.push(url);
onClose();
setQuery("");

View File

@@ -1,4 +1,6 @@
import { useTheme } from "next-themes";
import { useRouter } from "next/navigation";
import { env } from "next-runtime-env";
import { useEffect, useRef, useState } from "react";
import {
TbLayoutSidebarLeftCollapse,
@@ -43,6 +45,7 @@ export default function Dashboard({
const { resolvedTheme } = useTheme();
const { openModal } = useModal();
const { availableWorkspaces, hasLoaded } = useWorkspace();
const router = useRouter();
const { data: session, isPending: sessionLoading } = authClient.useSession();
const { data: user, isLoading: userLoading } = api.user.getUser.useQuery(
@@ -98,9 +101,13 @@ export default function Dashboard({
useEffect(() => {
if (hasLoaded && availableWorkspaces.length === 0) {
openModal("NEW_WORKSPACE", undefined, undefined, false);
if (env("NEXT_PUBLIC_KAN_ENV") === "cloud") {
router.push(`/onboarding/select-plan?returnUrl=${encodeURIComponent(window.location.pathname)}`);
} else {
openModal("NEW_WORKSPACE", undefined, undefined, false);
}
}
}, [hasLoaded, availableWorkspaces.length, openModal]);
}, [hasLoaded, availableWorkspaces.length, openModal, router]);
const isDarkMode = resolvedTheme === "dark";

View File

@@ -0,0 +1,32 @@
import { t } from "@lingui/core/macro";
import { HiOutlineAdjustmentsHorizontal } from "react-icons/hi2";
import { useFontSize, type FontSize } from "~/providers/font-size";
const fontSizeOptions: { value: FontSize; label: () => string }[] = [
{ value: "small", label: () => t`Small` },
{ value: "medium", label: () => t`Medium` },
{ value: "large", label: () => t`Large` },
];
export function FontSizeSelector() {
const { fontSize, setFontSize } = useFontSize();
return (
<div className="relative">
<HiOutlineAdjustmentsHorizontal className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<select
id="font-size-select"
value={fontSize}
onChange={(e) => setFontSize(e.target.value as FontSize)}
className="block w-full max-w-[180px] rounded-lg border-0 bg-light-50 pl-10 text-sm shadow-sm ring-1 ring-inset ring-light-300 focus:ring-2 focus:ring-inset focus:ring-light-400 dark:bg-dark-50 dark:text-dark-1000 dark:ring-dark-300 dark:focus:ring-dark-500"
>
{fontSizeOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label()}
</option>
))}
</select>
</div>
);
}

View File

@@ -88,7 +88,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
</button>
)}
{iconRight && type !== "password" && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<div className="absolute right-3 top-1/2 -translate-y-1/2 leading-[0]">
{iconRight}
</div>
)}

View File

@@ -39,7 +39,7 @@ export function LabelForm({
labelPublicId: entityId,
},
{
enabled: isEdit && !!entityId,
enabled: !!isEdit && entityId.length >= 12,
},
);
@@ -75,7 +75,7 @@ export function LabelForm({
reset(newFormState);
}
} catch (e) {
console.log(e);
console.error(e);
}
},
});
@@ -231,9 +231,7 @@ export function LabelForm({
<Button
type="submit"
isLoading={updateLabel.isPending || createLabel.isPending}
disabled={
!watch("name")
}
disabled={!watch("name")}
>
{isEdit ? t`Update label` : t`Create label`}
</Button>

View File

@@ -127,7 +127,7 @@ export function NewWorkspaceForm() {
body: JSON.stringify({
slug: slug || undefined,
workspacePublicId: values.publicId,
cancelUrl: "/settings/workspace?upgrade=pro",
cancelUrl: window.location.pathname + window.location.search,
successUrl: "/boards",
}),
},

View File

@@ -0,0 +1,130 @@
import Placeholder from "@tiptap/extension-placeholder";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useEffect, useRef } from "react";
import { twMerge } from "tailwind-merge";
interface PlainTextEditorProps {
content: string;
onChange?: (value: string) => void;
onBlur?: (value: string) => void;
onEnter?: (value: string) => void;
onEscape?: () => void;
readOnly?: boolean;
placeholder?: string;
className?: string;
}
export default function PlainTextEditor({
content,
onChange,
onBlur,
onEnter,
onEscape,
readOnly = false,
placeholder,
className,
}: PlainTextEditorProps) {
const onEnterRef = useRef(onEnter);
const onEscapeRef = useRef(onEscape);
const onBlurRef = useRef(onBlur);
const onChangeRef = useRef(onChange);
const contentRef = useRef(content);
useEffect(() => {
onEnterRef.current = onEnter;
}, [onEnter]);
useEffect(() => {
onEscapeRef.current = onEscape;
}, [onEscape]);
useEffect(() => {
onBlurRef.current = onBlur;
}, [onBlur]);
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
useEffect(() => {
contentRef.current = content;
}, [content]);
const editor = useEditor(
{
extensions: [
StarterKit.configure({
bold: false,
italic: false,
strike: false,
code: false,
codeBlock: false,
blockquote: false,
heading: false,
bulletList: false,
orderedList: false,
listItem: false,
horizontalRule: false,
hardBreak: false,
}),
Placeholder.configure({ placeholder }),
],
content,
editable: !readOnly,
onUpdate: ({ editor }) => onChangeRef.current?.(editor.getText()),
onBlur: ({ editor }) => onBlurRef.current?.(editor.getText()),
editorProps: {
handleKeyDown: (view, event) => {
if (event.key === "Enter") {
event.preventDefault();
onEnterRef.current?.(view.state.doc.textContent.trim());
return true;
}
if (event.key === "Escape") {
event.preventDefault();
// Reset to original content before calling the callback
editor?.commands.setContent(contentRef.current, false);
editor?.commands.blur();
onEscapeRef.current?.();
return true;
}
return false;
},
attributes: {
class: "outline-none focus:outline-none focus-visible:ring-0",
},
},
},
[],
);
useEffect(() => {
if (!editor) return;
if (content !== editor.getText()) {
editor.commands.setContent(content, false);
}
}, [content, editor]);
useEffect(() => {
if (!editor) return;
editor.setEditable(!readOnly);
}, [readOnly, editor]);
return (
<>
<style jsx global>{`
.plain-text-editor p.is-empty::before {
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
color: var(--placeholder-color, #9ca3af);
}
.plain-text-editor .tiptap p {
margin: 0 !important;
}
`}</style>
<EditorContent
editor={editor}
className={twMerge("plain-text-editor", className)}
/>
</>
);
}

View File

@@ -7,15 +7,17 @@ const Toggle = ({
label,
disabled,
showLabel = true,
labelPosition = "before",
}: {
isChecked: boolean;
onChange: () => void;
label: string;
disabled?: boolean;
showLabel?: boolean;
labelPosition?: "before" | "after";
}) => (
<div className="mr-4 flex items-center justify-end">
{showLabel && (
<div className="flex items-center">
{showLabel && labelPosition === "before" && (
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
{label}
</span>
@@ -38,6 +40,11 @@ const Toggle = ({
)}
/>
</Switch>
{showLabel && labelPosition === "after" && (
<span className="ml-2 text-xs text-light-900 dark:text-dark-900">
{label}
</span>
)}
</div>
);

View File

@@ -1,5 +1,7 @@
import { Button, Menu, Transition } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { useRouter } from "next/navigation";
import { env } from "next-runtime-env";
import { Fragment, useState } from "react";
import { HiCheck, HiMagnifyingGlass } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
@@ -18,6 +20,7 @@ export default function WorkspaceMenu({
const { workspace, isLoading, availableWorkspaces, switchWorkspace } =
useWorkspace();
const { openModal } = useModal();
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const { tooltipContent: commandPaletteShortcutTooltipContent } =
@@ -147,7 +150,11 @@ export default function WorkspaceMenu({
<div className="border-t-[1px] border-light-600 p-1 dark:border-dark-500">
<Menu.Item>
<button
onClick={() => openModal("NEW_WORKSPACE")}
onClick={() =>
env("NEXT_PUBLIC_KAN_ENV") === "cloud"
? router.push(`/onboarding/select-plan?returnUrl=${encodeURIComponent(window.location.pathname)}`)
: openModal("NEW_WORKSPACE")
}
className="flex w-full items-center justify-between rounded-[5px] px-3 py-2 text-left text-xs text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
>
{t`Create workspace`}

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} nicht gefunden"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Aktivitätsprotokolle"
},
"UGCIzB": {
"translation": "Label hinzufügen / bearbeiten",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Details hinzufügen..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Label hinzufügen"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Mitglied hinzufügen"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Karte"
},
"sU2uWc": {
"translation": "Karte dupliziert",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Passwort ändern"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Schriftgröße der Anwendung ändern."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Ändern Sie Ihre Spracheinstellungen."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Karten-Link kopieren"
},
"0utuU6": {
"translation": "Checklisten kopieren",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Labels kopieren",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Link zur Karte kopieren",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Mitglieder kopieren",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Label erstellen"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Neues Label erstellen"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Neue Liste erstellen"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Account löschen"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Karte löschen"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Anzeigename"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Morgen fällig"
},
"euc6Ns": {
"translation": "Duplizieren",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Karte duplizieren",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Wird dupliziert…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "E-Mail"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "E-Mail-Sichtbarkeit"
},
"bFREhu": {
"translation": "Ende der Liste",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Finden Sie Antworten auf häufig gestellte Fragen zu Kan. Können Sie nicht finden, wonach Sie suchen? <0>Kontaktieren Sie uns</0> gerne."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Schriftgröße"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Legen Sie los, indem Sie eine neue Liste erstellen"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "in"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Mitglied einladen"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Sprache"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Groß"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Liste"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Vorlage erstellen"
},
"hB02vO": {
"translation": "Mitglieder verwalten",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "hat Checklistenelement <0>{0}</0> als unvollständig markiert"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Mittel"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Erwähnungen"
},
"hty0d5": {
"translation": "Montag",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Montag"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "Monatliche Abrechnung"
},
"51UCsN": {
"translation": "In andere Liste verschieben",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "In Liste verschieben",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Neue Liste"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Keine Listen"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Es wurden noch keine Listen erstellt"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "Keine Ergebnisse für \"{debouncedQuery}\" gefunden."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "Sobald Sie Ihr Konto löschen, gibt es kein Zurück mehr. Diese Aktion kann nicht rückgängig gemacht werden."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Bitte versuchen Sie es erneut."
},
"nYNMZZ": {
"translation": "Position in Liste (0 = erste, leer lassen für Ende)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Profilbild"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Samstag",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Samstag"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Sehen Sie, was unsere Nutzer über Kan sagen"
},
"zYRVNp": {
"translation": "Liste auswählen",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Fälligkeitsdatum festlegen"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Einstellungen | Konto"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Klein"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Erfolg"
},
"DBC3t5": {
"translation": "Sonntag",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Sonntag"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Vorlage"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Das Board wurde wiederhergestellt."
},
"nKBUeF": {
"translation": "Die Karte wurde dupliziert.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Titel"
},
"wK4OTM": {
"translation": "Titel (optional)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "Checklistenelement konnte nicht gelöscht werden"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "Kommentar konnte nicht gelöscht werden"
},
"Oo4E6p": {
"translation": "Karte konnte nicht dupliziert werden",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "Karte konnte nicht verschoben werden",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "Karte konnte nicht aktualisiert werden"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "Checklisten-Element konnte nicht aktualisiert werden"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "Liste konnte nicht aktualisiert werden"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Label aktualisieren"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Webhooks"
},
"xLTZVf": {
"translation": "Wochenbeginn",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "Wochenbeginn"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Sie sind dabei, Ihr Passwort zu ändern."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -123,7 +123,7 @@
"origin": [
[
"src/views/board/index.tsx",
440
556
]
],
"translation": "{0} not found"
@@ -400,6 +400,18 @@
],
"translation": "Activity logs"
},
"UGCIzB": {
"translation": "Add / edit label",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextMenu.tsx",
50
]
]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -471,7 +483,7 @@
"origin": [
[
"src/views/card/components/ChecklistItemRow.tsx",
200
169
]
],
"translation": "Add details..."
@@ -483,11 +495,11 @@
"origin": [
[
"src/views/card/components/LabelSelector.tsx",
113
114
],
[
"src/views/card/components/LabelSelector.tsx",
118
119
]
],
"translation": "Add label"
@@ -499,7 +511,7 @@
"origin": [
[
"src/views/card/components/MemberSelector.tsx",
134
135
],
[
"src/views/members/components/InviteMemberForm.tsx",
@@ -1409,7 +1421,7 @@
"origin": [
[
"src/views/board/index.tsx",
413
529
],
[
"src/views/card/index.tsx",
@@ -2051,6 +2063,10 @@
"src/components/YouTubeEmbed/EditYouTubeModal.tsx",
176
],
[
"src/views/board/components/CardContextDuplicateModal.tsx",
267
],
[
"src/views/board/components/DeleteBoardConfirmation.tsx",
44
@@ -2118,6 +2134,18 @@
],
"translation": "Card"
},
"sU2uWc": {
"translation": "Card duplicated",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
67
]
]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -2151,6 +2179,10 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/index.tsx",
307
],
[
"src/views/card/components/Dropdown.tsx",
44
@@ -2169,11 +2201,11 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
77
88
],
[
"src/views/settings/AccountSettings.tsx",
87
98
],
[
"src/views/settings/components/ChangePasswordConfirmation.tsx",
@@ -2186,6 +2218,18 @@
],
"translation": "Change Password"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/settings/AccountSettings.tsx",
63
]
],
"translation": "Change the application font size."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
@@ -2193,7 +2237,7 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
52
53
]
],
"translation": "Change your language preferences."
@@ -2770,6 +2814,54 @@
],
"translation": "Copy card link"
},
"0utuU6": {
"translation": "Copy checklists",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
257
]
]
},
"fDQ7hA": {
"translation": "Copy labels",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
221
]
]
},
"1bJKhj": {
"translation": "Copy link to card",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextMenu.tsx",
62
]
]
},
"uvH2xS": {
"translation": "Copy members",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
239
]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -2873,7 +2965,7 @@
"origin": [
[
"src/components/LabelForm.tsx",
238
236
]
],
"translation": "Create label"
@@ -2933,7 +3025,7 @@
],
[
"src/views/card/components/LabelSelector.tsx",
100
101
]
],
"translation": "Create new label"
@@ -2945,11 +3037,11 @@
"origin": [
[
"src/views/board/index.tsx",
76
91
],
[
"src/views/board/index.tsx",
554
670
]
],
"translation": "Create new list"
@@ -3297,11 +3389,11 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
59
70
],
[
"src/views/settings/AccountSettings.tsx",
69
80
],
[
"src/views/settings/components/DeleteAccountConfirmation.tsx",
@@ -3327,6 +3419,10 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextMenu.tsx",
74
],
[
"src/views/card/components/Dropdown.tsx",
76
@@ -3513,7 +3609,7 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
35
36
]
],
"translation": "Display name"
@@ -3651,6 +3747,18 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDueDateModal.tsx",
36
],
[
"src/views/board/components/CardContextLabelsModal.tsx",
48
],
[
"src/views/board/components/CardContextMembersModal.tsx",
68
],
[
"src/views/boards/components/TemplateBoards.tsx",
17
@@ -3758,6 +3866,46 @@
],
"translation": "Due tomorrow"
},
"euc6Ns": {
"translation": "Duplicate",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
279
]
]
},
"BTDPLo": {
"translation": "Duplicate card",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
111
],
[
"src/views/board/components/CardContextMenu.tsx",
68
]
]
},
"06EQqT": {
"translation": "Duplicating…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
279
]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -3901,7 +4049,7 @@
],
[
"src/views/settings/AccountSettings.tsx",
42
43
]
],
"translation": "Email"
@@ -3942,6 +4090,18 @@
],
"translation": "Email visibility"
},
"bFREhu": {
"translation": "End of list",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
200
]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -4654,6 +4814,18 @@
],
"translation": "Find answers to common questions about Kan. Can't find what you're looking for? Feel free to <0>contact us</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/settings/AccountSettings.tsx",
60
]
],
"translation": "Font size"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -4857,7 +5029,7 @@
"origin": [
[
"src/views/board/index.tsx",
539
655
]
],
"translation": "Get started by creating a new list"
@@ -5365,7 +5537,7 @@
"origin": [
[
"src/components/CommandPallette.tsx",
168
167
]
],
"translation": "in"
@@ -5557,7 +5729,7 @@
"origin": [
[
"src/views/card/components/MemberSelector.tsx",
114
115
],
[
"src/views/members/components/InviteMemberForm.tsx",
@@ -5675,6 +5847,10 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextLabelsModal.tsx",
31
],
[
"src/views/board/components/Filters.tsx",
155
@@ -5721,11 +5897,23 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
49
50
]
],
"translation": "Language"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [
[
"src/components/FontSizeSelector.tsx",
9
]
],
"translation": "Large"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -5851,6 +6039,10 @@
"src/views/board/components/UpdateBoardSlugButton.tsx",
80
],
[
"src/views/board/index.tsx",
305
],
[
"src/views/card/components/Dropdown.tsx",
42
@@ -5871,6 +6063,10 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
117
],
[
"src/views/card/index.tsx",
124
@@ -6022,6 +6218,22 @@
],
"translation": "Make template"
},
"hB02vO": {
"translation": "Manage members",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextMembersModal.tsx",
51
],
[
"src/views/board/components/CardContextMenu.tsx",
38
]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -6050,6 +6262,18 @@
],
"translation": "marked checklist item <0>{0}</0> as incomplete"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [
[
"src/components/FontSizeSelector.tsx",
8
]
],
"translation": "Medium"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -6143,7 +6367,6 @@
"translation": "Mentions"
},
"hty0d5": {
"translation": "Monday",
"message": "Monday",
"placeholders": {},
"comments": [],
@@ -6152,7 +6375,8 @@
"src/views/settings/components/UpdateWeekStartDayForm.tsx",
53
]
]
],
"translation": "Monday"
},
"+8Nek/": {
"message": "Monthly",
@@ -6182,6 +6406,30 @@
],
"translation": "monthly billing"
},
"51UCsN": {
"translation": "Move to another list",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextMenu.tsx",
44
]
]
},
"J4+OTA": {
"translation": "Move to list",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextMoveListModal.tsx",
70
]
]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -6380,7 +6628,7 @@
],
[
"src/views/board/index.tsx",
503
619
]
],
"translation": "New list"
@@ -6588,7 +6836,7 @@
"origin": [
[
"src/views/board/index.tsx",
535
651
]
],
"translation": "No lists"
@@ -6600,7 +6848,7 @@
"origin": [
[
"src/views/board/index.tsx",
540
656
]
],
"translation": "No lists have been created yet"
@@ -6628,7 +6876,7 @@
"origin": [
[
"src/components/CommandPallette.tsx",
184
183
]
],
"translation": "No results found for \"{debouncedQuery}\"."
@@ -6712,7 +6960,7 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
62
73
]
],
"translation": "Once you delete your account, there is no going back. This action cannot be undone."
@@ -7264,11 +7512,11 @@
],
[
"src/views/board/index.tsx",
194
217
],
[
"src/views/board/index.tsx",
250
273
],
[
"src/views/boards/components/ImportBoardsForm.tsx",
@@ -7284,11 +7532,11 @@
],
[
"src/views/card/components/ChecklistItemRow.tsx",
69
67
],
[
"src/views/card/components/ChecklistItemRow.tsx",
97
95
],
[
"src/views/card/components/ChecklistNameInput.tsx",
@@ -7438,6 +7686,18 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
76
],
[
"src/views/board/components/CardContextMoveListModal.tsx",
42
],
[
"src/views/board/index.tsx",
314
],
[
"src/views/card/components/Dropdown.tsx",
51
@@ -7453,6 +7713,18 @@
],
"translation": "Please try again."
},
"nYNMZZ": {
"translation": "Position in list (0 = first, leave empty for end)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
193
]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -7592,7 +7864,7 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
29
30
]
],
"translation": "Profile picture"
@@ -8045,7 +8317,6 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Saturday",
"message": "Saturday",
"placeholders": {},
"comments": [],
@@ -8054,7 +8325,8 @@
"src/views/settings/components/UpdateWeekStartDayForm.tsx",
54
]
]
],
"translation": "Saturday"
},
"tfDRzk": {
"message": "Save",
@@ -8172,6 +8444,18 @@
],
"translation": "See what our users are saying about Kan"
},
"zYRVNp": {
"translation": "Select a list",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
131
]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -8289,6 +8573,14 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDueDateModal.tsx",
20
],
[
"src/views/board/components/CardContextMenu.tsx",
56
],
[
"src/views/card/components/DueDateSelector.tsx",
121
@@ -8331,7 +8623,7 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
25
26
]
],
"translation": "Settings | Account"
@@ -8548,6 +8840,18 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [
[
"src/components/FontSizeSelector.tsx",
7
]
],
"translation": "Small"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -8665,7 +8969,6 @@
"translation": "Success"
},
"DBC3t5": {
"translation": "Sunday",
"message": "Sunday",
"placeholders": {},
"comments": [],
@@ -8674,7 +8977,8 @@
"src/views/settings/components/UpdateWeekStartDayForm.tsx",
52
]
]
],
"translation": "Sunday"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -8771,11 +9075,11 @@
"origin": [
[
"src/views/board/index.tsx",
413
529
],
[
"src/views/board/index.tsx",
449
565
]
],
"translation": "Template"
@@ -8932,6 +9236,18 @@
],
"translation": "The board has been unarchived."
},
"nKBUeF": {
"translation": "The card has been duplicated.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
69
]
]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -9220,6 +9536,18 @@
],
"translation": "Title"
},
"wK4OTM": {
"translation": "Title (optional)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
180
]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -9389,6 +9717,10 @@
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/index.tsx",
312
],
[
"src/views/card/components/Dropdown.tsx",
49
@@ -9511,7 +9843,7 @@
"origin": [
[
"src/views/card/components/ChecklistItemRow.tsx",
96
94
]
],
"translation": "Unable to delete checklist item"
@@ -9528,6 +9860,30 @@
],
"translation": "Unable to delete comment"
},
"Oo4E6p": {
"translation": "Unable to duplicate card",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextDuplicateModal.tsx",
75
]
]
},
"K7k9u3": {
"translation": "Unable to move card",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/CardContextMoveListModal.tsx",
41
]
]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -9619,7 +9975,7 @@
"origin": [
[
"src/views/board/index.tsx",
249
272
],
[
"src/views/card/index.tsx",
@@ -9647,7 +10003,7 @@
"origin": [
[
"src/views/card/components/ChecklistItemRow.tsx",
68
66
]
],
"translation": "Unable to update checklist item"
@@ -9695,7 +10051,7 @@
"origin": [
[
"src/views/board/index.tsx",
193
216
],
[
"src/views/card/components/ListSelector.tsx",
@@ -10007,7 +10363,7 @@
"origin": [
[
"src/components/LabelForm.tsx",
238
236
]
],
"translation": "Update label"
@@ -10497,7 +10853,6 @@
"translation": "Webhooks"
},
"xLTZVf": {
"translation": "Week start day",
"message": "Week start day",
"placeholders": {},
"comments": [],
@@ -10506,7 +10861,8 @@
"src/views/settings/WorkspaceSettings.tsx",
91
]
]
],
"translation": "Week start day"
},
"9eF5oV": {
"message": "Welcome back",
@@ -10579,7 +10935,7 @@
],
[
"src/views/board/index.tsx",
413
529
],
[
"src/views/boards/index.tsx",
@@ -10867,7 +11223,7 @@
"origin": [
[
"src/views/settings/AccountSettings.tsx",
80
91
]
],
"translation": "You are about to change your password."
@@ -10927,11 +11283,11 @@
],
[
"src/views/board/index.tsx",
487
603
],
[
"src/views/board/index.tsx",
545
661
],
[
"src/views/boards/components/BoardsList.tsx",

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} no encontrado"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Registros de actividad"
},
"UGCIzB": {
"translation": "Añadir / editar etiqueta",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Añadir detalles..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Añadir etiqueta"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Añadir miembro"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Tarjeta"
},
"sU2uWc": {
"translation": "Tarjeta duplicada",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Cambiar contraseña"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Cambia el tamaño de fuente de la aplicación."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Cambia tus preferencias de idioma."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Copiar enlace de la tarjeta"
},
"0utuU6": {
"translation": "Copiar listas de verificación",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Copiar etiquetas",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Copiar enlace a la tarjeta",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Copiar miembros",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Crear etiqueta"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Crear nueva etiqueta"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Crear nueva lista"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Eliminar cuenta"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Eliminar tarjeta"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Nombre para mostrar"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Vence mañana"
},
"euc6Ns": {
"translation": "Duplicar",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Duplicar tarjeta",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Duplicando…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "Email"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "Visibilidad del email"
},
"bFREhu": {
"translation": "Fin de la lista",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Encuentra respuestas a preguntas frecuentes sobre Kan. ¿No encuentras lo que buscas? No dudes en <0>contactarnos</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Tamaño de fuente"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Comienza creando una nueva lista"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "en"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Invitar miembro"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Idioma"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Grande"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Lista"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Crear plantilla"
},
"hB02vO": {
"translation": "Gestionar miembros",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "marcó el elemento de la lista de verificación <0>{0}</0> como incompleto"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Mediano"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Menciones"
},
"hty0d5": {
"translation": "Lunes",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Lunes"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "facturación mensual"
},
"51UCsN": {
"translation": "Mover a otra lista",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "Mover a lista",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Nueva lista"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Sin listas"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Aún no se han creado listas"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "No se encontraron resultados para \"{debouncedQuery}\"."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "Una vez que elimines tu cuenta, no hay vuelta atrás. Esta acción no se puede deshacer."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Por favor, inténtalo de nuevo."
},
"nYNMZZ": {
"translation": "Posición en la lista (0 = primero, dejar vacío para el final)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Foto de perfil"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Sábado",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Sábado"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Mira lo que nuestros usuarios dicen sobre Kan"
},
"zYRVNp": {
"translation": "Seleccionar una lista",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Establecer fecha de vencimiento"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Configuración | Cuenta"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Pequeño"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Éxito"
},
"DBC3t5": {
"translation": "Domingo",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Domingo"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Plantilla"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "El tablero ha sido desarchivado."
},
"nKBUeF": {
"translation": "La tarjeta ha sido duplicada.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Título"
},
"wK4OTM": {
"translation": "Título (opcional)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "No se pudo eliminar el elemento de la lista de verificación"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "No se pudo eliminar el comentario"
},
"Oo4E6p": {
"translation": "No se puede duplicar la tarjeta",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "No se puede mover la tarjeta",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "No se pudo actualizar la tarjeta"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "No se pudo actualizar el elemento de la lista de verificación"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "No se pudo actualizar la lista"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Actualizar etiqueta"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Webhooks"
},
"xLTZVf": {
"translation": "Día de inicio de semana",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "Día de inicio de semana"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Estás a punto de cambiar tu contraseña."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} introuvable"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Journaux d'activité"
},
"UGCIzB": {
"translation": "Ajouter / modifier l'étiquette",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Ajouter des détails..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Ajouter une étiquette"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Ajouter un membre"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Carte"
},
"sU2uWc": {
"translation": "Carte dupliquée",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Changer le mot de passe"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Modifier la taille de police de l'application."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Modifiez vos préférences de langue."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Copier le lien de la carte"
},
"0utuU6": {
"translation": "Copier les listes de contrôle",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Copier les étiquettes",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Copier le lien vers la carte",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Copier les membres",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Créer une étiquette"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Créer une nouvelle étiquette"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Créer une nouvelle liste"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Supprimer le compte"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Supprimer la carte"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Nom d'affichage"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Échéance demain"
},
"euc6Ns": {
"translation": "Dupliquer",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Dupliquer la carte",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Duplication en cours…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "E-mail"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "Visibilité de l'e-mail"
},
"bFREhu": {
"translation": "Fin de la liste",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Trouvez des réponses aux questions courantes sur Kan. Vous ne trouvez pas ce que vous cherchez ? N'hésitez pas à <0>nous contacter</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Taille de police"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Commencez en créant une nouvelle liste"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "dans"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Inviter un membre"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Langue"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Grande"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Liste"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Créer un modèle"
},
"hB02vO": {
"translation": "Gérer les membres",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "a marqué l'élément de checklist <0>{0}</0> comme incomplet"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Moyenne"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Mentions"
},
"hty0d5": {
"translation": "Lundi",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Lundi"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "facturation mensuelle"
},
"51UCsN": {
"translation": "Déplacer vers une autre liste",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "Déplacer vers la liste",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Nouvelle liste"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Aucune liste"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Aucune liste n'a encore été créée"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "Aucun résultat trouvé pour « {debouncedQuery} »."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "Une fois votre compte supprimé, il n'y a pas de retour en arrière possible. Cette action ne peut pas être annulée."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Veuillez réessayer."
},
"nYNMZZ": {
"translation": "Position dans la liste (0 = premier, laisser vide pour la fin)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Photo de profil"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Samedi",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Samedi"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Découvrez ce que nos utilisateurs disent de Kan"
},
"zYRVNp": {
"translation": "Sélectionner une liste",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Définir la date d'échéance"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Paramètres | Compte"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Petite"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Succès"
},
"DBC3t5": {
"translation": "Dimanche",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Dimanche"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Modèle"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Le tableau a été désarchivé."
},
"nKBUeF": {
"translation": "La carte a été dupliquée.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Titre"
},
"wK4OTM": {
"translation": "Titre (facultatif)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "Impossible de supprimer l'élément de liste de contrôle"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "Impossible de supprimer le commentaire"
},
"Oo4E6p": {
"translation": "Impossible de dupliquer la carte",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "Impossible de déplacer la carte",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "Impossible de mettre à jour la carte"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "Impossible de mettre à jour l'élément de la checklist"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "Impossible de mettre à jour la liste"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Mettre à jour l'étiquette"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Webhooks"
},
"xLTZVf": {
"translation": "Jour de début de semaine",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "Jour de début de semaine"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Vous êtes sur le point de modifier votre mot de passe."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} non trovato"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Registri attività"
},
"UGCIzB": {
"translation": "Aggiungi / modifica etichetta",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Aggiungi dettagli..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Aggiungi etichetta"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Aggiungi membro"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Card"
},
"sU2uWc": {
"translation": "Scheda duplicata",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Cambia password"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Modifica la dimensione del carattere dell'applicazione."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Modifica le tue preferenze di lingua."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Copia link card"
},
"0utuU6": {
"translation": "Copia checklist",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Copia etichette",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Copia link alla scheda",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Copia membri",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Crea etichetta"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Crea nuova etichetta"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Crea nuova lista"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Elimina account"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Elimina card"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Nome visualizzato"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Scadenza domani"
},
"euc6Ns": {
"translation": "Duplica",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Duplica scheda",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Duplicazione in corso…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "Email"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "Visibilità email"
},
"bFREhu": {
"translation": "Fine dell'elenco",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Trova risposte alle domande più comuni su Kan. Non trovi quello che cerchi? Sentiti libero di <0>contattarci</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Dimensione del carattere"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Inizia creando una nuova lista"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "in"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Invita membro"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Lingua"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Grande"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Lista"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Crea template"
},
"hB02vO": {
"translation": "Gestisci membri",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "ha contrassegnato l'elemento della checklist <0>{0}</0> come incompleto"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Medio"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Menzioni"
},
"hty0d5": {
"translation": "Lunedì",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Lunedì"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "fatturazione mensile"
},
"51UCsN": {
"translation": "Sposta in un altro elenco",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "Sposta nell'elenco",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Nuova lista"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Nessuna lista"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Nessuna lista è stata ancora creata"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "Nessun risultato trovato per \"{debouncedQuery}\"."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "Una volta eliminato il tuo account, non si può tornare indietro. Questa azione non può essere annullata."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Riprova."
},
"nYNMZZ": {
"translation": "Posizione nell'elenco (0 = primo, lascia vuoto per la fine)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Immagine del profilo"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Sabato",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Sabato"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Scopri cosa dicono i nostri utenti su Kan"
},
"zYRVNp": {
"translation": "Seleziona un elenco",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Imposta scadenza"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Impostazioni | Account"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Piccolo"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Successo"
},
"DBC3t5": {
"translation": "Domenica",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Domenica"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Template"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "La bacheca è stata ripristinata."
},
"nKBUeF": {
"translation": "La scheda è stata duplicata.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Titolo"
},
"wK4OTM": {
"translation": "Titolo (facoltativo)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "Impossibile eliminare l'elemento della checklist"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "Impossibile eliminare il commento"
},
"Oo4E6p": {
"translation": "Impossibile duplicare la scheda",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "Impossibile spostare la scheda",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "Impossibile aggiornare la card"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "Impossibile aggiornare l'elemento della checklist"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "Impossibile aggiornare la lista"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Aggiorna etichetta"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Webhook"
},
"xLTZVf": {
"translation": "Giorno di inizio settimana",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "Giorno di inizio settimana"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Stai per modificare la tua password."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} niet gevonden"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Activiteitenlogboeken"
},
"UGCIzB": {
"translation": "Label toevoegen / bewerken",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Voeg details toe..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Label toevoegen"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Lid toevoegen"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Kaart"
},
"sU2uWc": {
"translation": "Kaart gedupliceerd",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Wachtwoord wijzigen"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Pas de lettergrootte van de applicatie aan."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Wijzig je taalvoorkeuren."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Kopieer kaartlink"
},
"0utuU6": {
"translation": "Checklists kopiëren",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Labels kopiëren",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Link naar kaart kopiëren",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Leden kopiëren",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Maak label"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Maak nieuw label"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Maak nieuwe lijst"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Account verwijderen"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Kaart verwijderen"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Weergavenaam"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Verloopt morgen"
},
"euc6Ns": {
"translation": "Dupliceren",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Kaart dupliceren",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Dupliceren…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "E-mail"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "E-mailzichtbaarheid"
},
"bFREhu": {
"translation": "Einde van lijst",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Vind antwoorden op veelgestelde vragen over Kan. Kun je niet vinden wat je zoekt? Neem gerust <0>contact met ons op</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Lettergrootte"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Ga aan de slag door een nieuwe lijst te maken"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "in"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Lid uitnodigen"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Taal"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Groot"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Lijst"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Sjabloon maken"
},
"hB02vO": {
"translation": "Leden beheren",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "heeft checklistitem <0>{0}</0> als onvolledig gemarkeerd"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Gemiddeld"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Vermeldingen"
},
"hty0d5": {
"translation": "Maandag",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Maandag"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "maandelijkse facturering"
},
"51UCsN": {
"translation": "Verplaats naar andere lijst",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "Verplaats naar lijst",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Nieuwe lijst"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Geen lijsten"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Er zijn nog geen lijsten aangemaakt"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "Geen resultaten gevonden voor \"{debouncedQuery}\"."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "Zodra je je account verwijdert, is er geen weg terug. Deze actie kan niet ongedaan worden gemaakt."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Probeer het opnieuw."
},
"nYNMZZ": {
"translation": "Positie in lijst (0 = eerste, leeg laten voor het einde)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Profielfoto"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Zaterdag",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Zaterdag"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Bekijk wat onze gebruikers over Kan zeggen"
},
"zYRVNp": {
"translation": "Selecteer een lijst",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Vervaldatum instellen"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Instellingen | Account"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Klein"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Succes"
},
"DBC3t5": {
"translation": "Zondag",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Zondag"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Sjabloon"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Het board is gedearchiveerd."
},
"nKBUeF": {
"translation": "De kaart is gedupliceerd.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Titel"
},
"wK4OTM": {
"translation": "Titel (optioneel)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "Kan checklistitem niet verwijderen"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "Kan opmerking niet verwijderen"
},
"Oo4E6p": {
"translation": "Kan kaart niet dupliceren",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "Kan kaart niet verplaatsen",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "Kan kaart niet bijwerken"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "Kan checklistitem niet bijwerken"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "Kan lijst niet bijwerken"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Label bijwerken"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Webhooks"
},
"xLTZVf": {
"translation": "Eerste dag van de week",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "Eerste dag van de week"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Je staat op het punt je wachtwoord te wijzigen."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} nie znaleziono"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Dzienniki aktywności"
},
"UGCIzB": {
"translation": "Dodaj / edytuj etykietę",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Dodaj szczegóły..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Dodaj etykietę"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Dodaj członka"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Karta"
},
"sU2uWc": {
"translation": "Karta zduplikowana",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Zmień hasło"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Zmień rozmiar czcionki aplikacji."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Zmień preferencje językowe."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Kopiuj link do karty"
},
"0utuU6": {
"translation": "Kopiuj listy kontrolne",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Kopiuj etykiety",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Kopiuj link do karty",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Kopiuj członków",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Utwórz etykietę"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Utwórz nową etykietę"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Utwórz nową listę"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Usuń konto"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Usuń kartę"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Nazwa wyświetlana"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Termin jutro"
},
"euc6Ns": {
"translation": "Duplikuj",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Duplikuj kartę",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Duplikowanie…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "E-mail"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "Widoczność e-maila"
},
"bFREhu": {
"translation": "Koniec listy",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Znajdź odpowiedzi na najczęstsze pytania dotyczące Kan. Nie możesz znaleźć tego, czego szukasz? Śmiało <0>skontaktuj się z nami</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Rozmiar czcionki"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Rozpocznij, tworząc nową listę"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "w"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Zaproś członka"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Język"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Duży"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Lista"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Utwórz szablon"
},
"hB02vO": {
"translation": "Zarządzaj członkami",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "oznaczył(a) element listy kontrolnej <0>{0}</0> jako nieukończony"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Średni"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Wzmianki"
},
"hty0d5": {
"translation": "Poniedziałek",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Poniedziałek"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "rozliczenie miesięczne"
},
"51UCsN": {
"translation": "Przenieś do innej listy",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "Przenieś do listy",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Nowa lista"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Brak list"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Nie utworzono jeszcze żadnych list"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "Nie znaleziono wyników dla „{debouncedQuery}”."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "Po usunięciu konta nie będzie można tego cofnąć. Tej operacji nie da się odwrócić."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Spróbuj ponownie."
},
"nYNMZZ": {
"translation": "Pozycja na liście (0 = pierwsza, puste pole = koniec)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Zdjęcie profilowe"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Sobota",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Sobota"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Zobacz, co nasi użytkownicy mówią o Kan"
},
"zYRVNp": {
"translation": "Wybierz listę",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Ustaw termin"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Ustawienia | Konto"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Mały"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Sukces"
},
"DBC3t5": {
"translation": "Niedziela",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Niedziela"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Szablon"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Tablica została przywrócona z archiwum."
},
"nKBUeF": {
"translation": "Karta została zduplikowana.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Tytuł"
},
"wK4OTM": {
"translation": "Tytuł (opcjonalnie)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "Nie można usunąć elementu listy kontrolnej"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "Nie można usunąć komentarza"
},
"Oo4E6p": {
"translation": "Nie można zduplikować karty",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "Nie można przenieść karty",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "Nie można zaktualizować karty"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "Nie można zaktualizować elementu listy kontrolnej"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "Nie można zaktualizować listy"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Aktualizuj etykietę"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Webhooki"
},
"xLTZVf": {
"translation": "Pierwszy dzień tygodnia",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "Pierwszy dzień tygodnia"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Zaraz zmienisz swoje hasło."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} não encontrado"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Registros de atividades"
},
"UGCIzB": {
"translation": "Adicionar / editar etiqueta",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Adicionar detalhes..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Adicionar etiqueta"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Adicionar membro"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Cartão"
},
"sU2uWc": {
"translation": "Cartão duplicado",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Alterar senha"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Altere o tamanho da fonte do aplicativo."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Altere suas preferências de idioma."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Copiar link do cartão"
},
"0utuU6": {
"translation": "Copiar checklists",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Copiar etiquetas",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Copiar link do cartão",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Copiar membros",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Criar etiqueta"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Criar nova etiqueta"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Criar nova lista"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Excluir conta"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Excluir cartão"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Nome de exibição"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Vence amanhã"
},
"euc6Ns": {
"translation": "Duplicar",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Duplicar cartão",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Duplicando…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "Email"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "Visibilidade do email"
},
"bFREhu": {
"translation": "Fim da lista",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Encontre respostas para perguntas comuns sobre o Kan. Não encontrou o que procura? Sinta-se à vontade para <0>entrar em contato</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Tamanho da fonte"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Comece criando uma nova lista"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "em"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Convidar membro"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Idioma"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Grande"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Lista"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Criar modelo"
},
"hB02vO": {
"translation": "Gerenciar membros",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "marcou o item da checklist <0>{0}</0> como incompleto"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Médio"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Menções"
},
"hty0d5": {
"translation": "Segunda-feira",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Segunda-feira"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "cobrança mensal"
},
"51UCsN": {
"translation": "Mover para outra lista",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "Mover para lista",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Nova lista"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Nenhuma lista"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Nenhuma lista foi criada ainda"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "Nenhum resultado encontrado para \"{debouncedQuery}\"."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "Depois de excluir sua conta, não há como voltar atrás. Esta ação não pode ser desfeita."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Por favor, tente novamente."
},
"nYNMZZ": {
"translation": "Posição na lista (0 = primeira, deixe vazio para o final)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Foto de perfil"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Sábado",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Sábado"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Veja o que nossos usuários estão dizendo sobre o Kan"
},
"zYRVNp": {
"translation": "Selecione uma lista",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Definir data de vencimento"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Configurações | Conta"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Pequeno"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Sucesso"
},
"DBC3t5": {
"translation": "Domingo",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Domingo"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Modelo"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "O quadro foi desarquivado."
},
"nKBUeF": {
"translation": "O cartão foi duplicado.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Título"
},
"wK4OTM": {
"translation": "Título (opcional)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "Não foi possível excluir item da checklist"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "Não foi possível excluir o comentário"
},
"Oo4E6p": {
"translation": "Não foi possível duplicar o cartão",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "Não foi possível mover o cartão",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "Não foi possível atualizar o cartão"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "Não foi possível atualizar o item da checklist"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "Não foi possível atualizar a lista"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Atualizar etiqueta"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Webhooks"
},
"xLTZVf": {
"translation": "Dia de início da semana",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "Dia de início da semana"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Você está prestes a alterar sua senha."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@
"0": ["isTemplate ? \"Template\" : \"Board\""]
},
"comments": [],
"origin": [["src/views/board/index.tsx", 440]],
"origin": [["src/views/board/index.tsx", 556]],
"translation": "{0} не найдено"
},
"0xWkkH": {
@@ -238,6 +238,13 @@
"origin": [["src/views/home/components/Features.tsx", 110]],
"translation": "Журналы активности"
},
"UGCIzB": {
"translation": "Добавить / изменить метку",
"message": "Add / edit label",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 50]]
},
"B3xxao": {
"message": "Add a card",
"placeholders": {},
@@ -280,7 +287,7 @@
"message": "Add details...",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 200]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 169]],
"translation": "Добавить детали..."
},
"lyqwgn": {
@@ -288,8 +295,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/LabelSelector.tsx", 113],
["src/views/card/components/LabelSelector.tsx", 118]
["src/views/card/components/LabelSelector.tsx", 114],
["src/views/card/components/LabelSelector.tsx", 119]
],
"translation": "Добавить метку"
},
@@ -298,7 +305,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 134],
["src/views/card/components/MemberSelector.tsx", 135],
["src/views/members/components/InviteMemberForm.tsx", 257]
],
"translation": "Добавить участника"
@@ -842,7 +849,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/card/index.tsx", 317],
["src/views/public/board/index.tsx", 125]
],
@@ -1235,6 +1242,7 @@
"origin": [
["src/components/DeleteLabelConfirmation.tsx", 49],
["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 176],
["src/views/board/components/CardContextDuplicateModal.tsx", 267],
["src/views/board/components/DeleteBoardConfirmation.tsx", 44],
["src/views/card/components/Comment.tsx", 195],
["src/views/card/components/DeleteCardConfirmation.tsx", 86],
@@ -1261,6 +1269,13 @@
"origin": [["src/views/card/index.tsx", 317]],
"translation": "Карточка"
},
"sU2uWc": {
"translation": "Карточка продублирована",
"message": "Card duplicated",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 67]]
},
"fEY2vP": {
"message": "Card not found",
"placeholders": {},
@@ -1283,6 +1298,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 307],
["src/views/card/components/Dropdown.tsx", 44],
["src/views/public/board/CardModal.tsx", 38]
],
@@ -1293,18 +1309,25 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 77],
["src/views/settings/AccountSettings.tsx", 87],
["src/views/settings/AccountSettings.tsx", 88],
["src/views/settings/AccountSettings.tsx", 98],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 109],
["src/views/settings/components/ChangePasswordConfirmation.tsx", 178]
],
"translation": "Сменить пароль"
},
"fzGyWs": {
"message": "Change the application font size.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 63]],
"translation": "Изменить размер шрифта приложения."
},
"yD3ZSw": {
"message": "Change your language preferences.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 52]],
"origin": [["src/views/settings/AccountSettings.tsx", 53]],
"translation": "Измените языковые настройки."
},
"gZxH/p": {
@@ -1654,6 +1677,40 @@
"origin": [["src/views/card/components/Dropdown.tsx", 58]],
"translation": "Скопировать ссылку на карточку"
},
"0utuU6": {
"translation": "Копировать чек-листы",
"message": "Copy checklists",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 257]
]
},
"fDQ7hA": {
"translation": "Копировать метки",
"message": "Copy labels",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 221]
]
},
"1bJKhj": {
"translation": "Копировать ссылку на карточку",
"message": "Copy link to card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 62]]
},
"uvH2xS": {
"translation": "Копировать участников",
"message": "Copy members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 239]
]
},
"9PaIxv": {
"message": "Core features",
"placeholders": {},
@@ -1715,7 +1772,7 @@
"message": "Create label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Создать метку"
},
"XTKtey": {
@@ -1750,7 +1807,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewCardForm.tsx", 417],
["src/views/card/components/LabelSelector.tsx", 100]
["src/views/card/components/LabelSelector.tsx", 101]
],
"translation": "Создать новую метку"
},
@@ -1759,8 +1816,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 76],
["src/views/board/index.tsx", 554]
["src/views/board/index.tsx", 91],
["src/views/board/index.tsx", 670]
],
"translation": "Создать новый список"
},
@@ -1961,8 +2018,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/settings/AccountSettings.tsx", 59],
["src/views/settings/AccountSettings.tsx", 69],
["src/views/settings/AccountSettings.tsx", 70],
["src/views/settings/AccountSettings.tsx", 80],
["src/views/settings/components/DeleteAccountConfirmation.tsx", 96]
],
"translation": "Удалить аккаунт"
@@ -1978,7 +2035,10 @@
"message": "Delete card",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/Dropdown.tsx", 76]],
"origin": [
["src/views/board/components/CardContextMenu.tsx", 74],
["src/views/card/components/Dropdown.tsx", 76]
],
"translation": "Удалить карточку"
},
"njJFtc": {
@@ -2086,7 +2146,7 @@
"message": "Display name",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 35]],
"origin": [["src/views/settings/AccountSettings.tsx", 36]],
"translation": "Отображаемое имя"
},
"MlyjJu": {
@@ -2164,6 +2224,9 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 36],
["src/views/board/components/CardContextLabelsModal.tsx", 48],
["src/views/board/components/CardContextMembersModal.tsx", 68],
["src/views/boards/components/TemplateBoards.tsx", 17],
["src/views/boards/components/TemplateBoards.tsx", 23],
["src/views/boards/components/TemplateBoards.tsx", 29],
@@ -2219,6 +2282,34 @@
"origin": [["src/views/board/components/Filters.tsx", 122]],
"translation": "Срок — завтра"
},
"euc6Ns": {
"translation": "Дублировать",
"message": "Duplicate",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"BTDPLo": {
"translation": "Дублировать карточку",
"message": "Duplicate card",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 111],
["src/views/board/components/CardContextMenu.tsx", 68]
]
},
"06EQqT": {
"translation": "Дублирование…",
"message": "Duplicating…",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 279]
]
},
"ePK91l": {
"message": "Edit",
"placeholders": {},
@@ -2304,7 +2395,7 @@
"comments": [],
"origin": [
["src/views/members/components/InviteMemberForm.tsx", 272],
["src/views/settings/AccountSettings.tsx", 42]
["src/views/settings/AccountSettings.tsx", 43]
],
"translation": "Email"
},
@@ -2331,6 +2422,15 @@
"origin": [["src/views/settings/WorkspaceSettings.tsx", 100]],
"translation": "Видимость email"
},
"bFREhu": {
"translation": "Конец списка",
"message": "End of list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 200]
]
},
"0+ewPp": {
"message": "Enhancement",
"placeholders": {},
@@ -2768,6 +2868,13 @@
"origin": [["src/views/home/components/Faqs.tsx", 150]],
"translation": "Найдите ответы на часто задаваемые вопросы о Kan. Не нашли то, что искали? Не стесняйтесь <0>связаться с нами</0>."
},
"q3il6U": {
"message": "Font size",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 60]],
"translation": "Размер шрифта"
},
"+3TYXa": {
"message": "For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.",
"placeholders": {},
@@ -2880,7 +2987,7 @@
"message": "Get started by creating a new list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 539]],
"origin": [["src/views/board/index.tsx", 655]],
"translation": "Начните с создания нового списка"
},
"oW13KZ": {
@@ -3187,7 +3294,7 @@
"message": "in",
"placeholders": {},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 168]],
"origin": [["src/components/CommandPallette.tsx", 167]],
"translation": "в"
},
"WbTDwg": {
@@ -3300,7 +3407,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/card/components/MemberSelector.tsx", 114],
["src/views/card/components/MemberSelector.tsx", 115],
["src/views/members/components/InviteMemberForm.tsx", 367]
],
"translation": "Пригласить участника"
@@ -3372,6 +3479,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextLabelsModal.tsx", 31],
["src/views/board/components/Filters.tsx", 155],
["src/views/board/components/NewCardForm.tsx", 421],
["src/views/card/index.tsx", 133]
@@ -3398,9 +3506,16 @@
"message": "Language",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 49]],
"origin": [["src/views/settings/AccountSettings.tsx", 50]],
"translation": "Язык"
},
"k7rCa/": {
"message": "Large",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 9]],
"translation": "Крупный"
},
"8Is1ih": {
"message": "Launch offer",
"placeholders": {},
@@ -3476,6 +3591,7 @@
"comments": [],
"origin": [
["src/views/board/components/UpdateBoardSlugButton.tsx", 80],
["src/views/board/index.tsx", 305],
["src/views/card/components/Dropdown.tsx", 42],
["src/views/public/board/CardModal.tsx", 36],
["src/views/public/board/index.tsx", 77]
@@ -3486,7 +3602,10 @@
"message": "List",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/index.tsx", 124]],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 117],
["src/views/card/index.tsx", 124]
],
"translation": "Список"
},
"zQX5Dj": {
@@ -3575,6 +3694,16 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Создать шаблон"
},
"hB02vO": {
"translation": "Управление участниками",
"message": "Manage members",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextMembersModal.tsx", 51],
["src/views/board/components/CardContextMenu.tsx", 38]
]
},
"IDvohQ": {
"message": "marked a checklist item as incomplete",
"placeholders": {},
@@ -3591,6 +3720,13 @@
"origin": [["src/views/card/components/ActivityList.tsx", 258]],
"translation": "отметил элемент контрольного списка <0>{0}</0> как невыполненный"
},
"agPptk": {
"message": "Medium",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 8]],
"translation": "Средний"
},
"W/Elkg": {
"message": "Medium Priority",
"placeholders": {},
@@ -3642,11 +3778,13 @@
"translation": "Упоминания"
},
"hty0d5": {
"translation": "Понедельник",
"message": "Monday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 53]
],
"translation": "Понедельник"
},
"+8Nek/": {
"message": "Monthly",
@@ -3665,6 +3803,20 @@
"origin": [["src/views/members/components/InviteMemberForm.tsx", 159]],
"translation": "ежемесячная оплата"
},
"51UCsN": {
"translation": "Переместить в другой список",
"message": "Move to another list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMenu.tsx", 44]]
},
"J4+OTA": {
"translation": "Переместить в список",
"message": "Move to list",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 70]]
},
"BOqTi5": {
"message": "moved the card from <0>{0}</0> to<1>{1}</1>",
"placeholders": {
@@ -3776,7 +3928,7 @@
"comments": [],
"origin": [
["src/views/board/components/NewListForm.tsx", 113],
["src/views/board/index.tsx", 503]
["src/views/board/index.tsx", 619]
],
"translation": "Новый список"
},
@@ -3902,14 +4054,14 @@
"message": "No lists",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 535]],
"origin": [["src/views/board/index.tsx", 651]],
"translation": "Нет списков"
},
"fvLNDy": {
"message": "No lists have been created yet",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/index.tsx", 540]],
"origin": [["src/views/board/index.tsx", 656]],
"translation": "Списки ещё не созданы"
},
"i30J2U": {
@@ -3925,7 +4077,7 @@
"debouncedQuery": ["debouncedQuery"]
},
"comments": [],
"origin": [["src/components/CommandPallette.tsx", 184]],
"origin": [["src/components/CommandPallette.tsx", 183]],
"translation": "По запросу «{debouncedQuery}» ничего не найдено."
},
"Q9pQaX": {
@@ -3976,7 +4128,7 @@
"message": "Once you delete your account, there is no going back. This action cannot be undone.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 62]],
"origin": [["src/views/settings/AccountSettings.tsx", 73]],
"translation": "После удаления аккаунта возврата не будет. Это действие необратимо."
},
"dmKdk0": {
@@ -4301,13 +4453,13 @@
["src/views/board/components/NewTemplateForm.tsx", 77],
["src/views/board/components/UpdateBoardSlugForm.tsx", 73],
["src/views/board/components/VisibilityButton.tsx", 57],
["src/views/board/index.tsx", 194],
["src/views/board/index.tsx", 250],
["src/views/board/index.tsx", 217],
["src/views/board/index.tsx", 273],
["src/views/boards/components/ImportBoardsForm.tsx", 243],
["src/views/boards/components/ImportBoardsForm.tsx", 395],
["src/views/card/components/AttachmentThumbnails.tsx", 74],
["src/views/card/components/ChecklistItemRow.tsx", 69],
["src/views/card/components/ChecklistItemRow.tsx", 97],
["src/views/card/components/ChecklistItemRow.tsx", 67],
["src/views/card/components/ChecklistItemRow.tsx", 95],
["src/views/card/components/ChecklistNameInput.tsx", 47],
["src/views/card/components/Checklists.tsx", 81],
["src/views/card/components/Comment.tsx", 93],
@@ -4357,12 +4509,24 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 76],
["src/views/board/components/CardContextMoveListModal.tsx", 42],
["src/views/board/index.tsx", 314],
["src/views/card/components/Dropdown.tsx", 51],
["src/views/public/board/CardModal.tsx", 45],
["src/views/public/board/index.tsx", 86]
],
"translation": "Пожалуйста, попробуйте ещё раз."
},
"nYNMZZ": {
"translation": "Позиция в списке (0 = первая, оставьте пустым для конца)",
"message": "Position in list (0 = first, leave empty for end)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 193]
]
},
"aHCEmh": {
"message": "Pricing",
"placeholders": {},
@@ -4438,7 +4602,7 @@
"message": "Profile picture",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 29]],
"origin": [["src/views/settings/AccountSettings.tsx", 30]],
"translation": "Фото профиля"
},
"7d1a0d": {
@@ -4707,11 +4871,13 @@
"translation": "SAML SSO"
},
"+5kO8P": {
"translation": "Суббота",
"message": "Saturday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 54]
],
"translation": "Суббота"
},
"tfDRzk": {
"message": "Save",
@@ -4782,6 +4948,15 @@
"origin": [["src/views/home/components/Testimonials.tsx", 239]],
"translation": "Посмотрите, что наши пользователи говорят о Kan"
},
"zYRVNp": {
"translation": "Выберите список",
"message": "Select a list",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 131]
]
},
"wgNoIs": {
"message": "Select all",
"placeholders": {},
@@ -4852,7 +5027,11 @@
"message": "Set due date",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/DueDateSelector.tsx", 121]],
"origin": [
["src/views/board/components/CardContextDueDateModal.tsx", 20],
["src/views/board/components/CardContextMenu.tsx", 56],
["src/views/card/components/DueDateSelector.tsx", 121]
],
"translation": "Установить срок"
},
"Eit43O": {
@@ -4876,7 +5055,7 @@
"message": "Settings | Account",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 25]],
"origin": [["src/views/settings/AccountSettings.tsx", 26]],
"translation": "Настройки | Аккаунт"
},
"iy1wb+": {
@@ -5006,6 +5185,13 @@
],
"translation": "SLA"
},
"4Revpc": {
"message": "Small",
"placeholders": {},
"comments": [],
"origin": [["src/components/FontSizeSelector.tsx", 7]],
"translation": "Мелкий"
},
"++rVAm": {
"message": "Social Media",
"placeholders": {},
@@ -5078,11 +5264,13 @@
"translation": "Успех"
},
"DBC3t5": {
"translation": "Воскресенье",
"message": "Sunday",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]]
"origin": [
["src/views/settings/components/UpdateWeekStartDayForm.tsx", 52]
],
"translation": "Воскресенье"
},
"uZg2+w": {
"message": "Supercharge your workspace for just $29/month. Here's what you'll get:",
@@ -5143,8 +5331,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 449]
["src/views/board/index.tsx", 529],
["src/views/board/index.tsx", 565]
],
"translation": "Шаблон"
},
@@ -5238,6 +5426,13 @@
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Доска была восстановлена из архива."
},
"nKBUeF": {
"translation": "Карточка была продублирована.",
"message": "The card has been duplicated.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 69]]
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5420,6 +5615,15 @@
"origin": [["src/components/YouTubeEmbed/EditYouTubeModal.tsx", 127]],
"translation": "Заголовок"
},
"wK4OTM": {
"translation": "Название (необязательно)",
"message": "Title (optional)",
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/CardContextDuplicateModal.tsx", 180]
]
},
"Se/UIp": {
"message": "To Do",
"placeholders": {},
@@ -5522,6 +5726,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 312],
["src/views/card/components/Dropdown.tsx", 49],
["src/views/public/board/CardModal.tsx", 43],
["src/views/public/board/index.tsx", 84]
@@ -5593,7 +5798,7 @@
"message": "Unable to delete checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 96]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 94]],
"translation": "Не удалось удалить элемент контрольного списка"
},
"2QGEbi": {
@@ -5603,6 +5808,20 @@
"origin": [["src/views/card/components/DeleteCommentConfirmation.tsx", 45]],
"translation": "Не удалось удалить комментарий"
},
"Oo4E6p": {
"translation": "Невозможно дублировать карточку",
"message": "Unable to duplicate card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextDuplicateModal.tsx", 75]]
},
"K7k9u3": {
"translation": "Невозможно переместить карточку",
"message": "Unable to move card",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/CardContextMoveListModal.tsx", 41]]
},
"8yFGGF": {
"message": "Unable to remove member",
"placeholders": {},
@@ -5661,7 +5880,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 249],
["src/views/board/index.tsx", 272],
["src/views/card/index.tsx", 231]
],
"translation": "Не удалось обновить карточку"
@@ -5677,7 +5896,7 @@
"message": "Unable to update checklist item",
"placeholders": {},
"comments": [],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 68]],
"origin": [["src/views/card/components/ChecklistItemRow.tsx", 66]],
"translation": "Не удалось обновить элемент контрольного списка"
},
"fYVH36": {
@@ -5706,7 +5925,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/index.tsx", 193],
["src/views/board/index.tsx", 216],
["src/views/card/components/ListSelector.tsx", 54]
],
"translation": "Не удалось обновить список"
@@ -5876,7 +6095,7 @@
"message": "Update label",
"placeholders": {},
"comments": [],
"origin": [["src/components/LabelForm.tsx", 238]],
"origin": [["src/components/LabelForm.tsx", 236]],
"translation": "Обновить метку"
},
"L5ZPjz": {
@@ -6168,11 +6387,11 @@
"translation": "Вебхуки"
},
"xLTZVf": {
"translation": "День начала недели",
"message": "Week start day",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]]
"origin": [["src/views/settings/WorkspaceSettings.tsx", 91]],
"translation": "День начала недели"
},
"9eF5oV": {
"message": "Welcome back",
@@ -6215,7 +6434,7 @@
"comments": [],
"origin": [
["src/components/SettingsLayout.tsx", 47],
["src/views/board/index.tsx", 413],
["src/views/board/index.tsx", 529],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
@@ -6394,7 +6613,7 @@
"message": "You are about to change your password.",
"placeholders": {},
"comments": [],
"origin": [["src/views/settings/AccountSettings.tsx", 80]],
"origin": [["src/views/settings/AccountSettings.tsx", 91]],
"translation": "Вы собираетесь изменить свой пароль."
},
"aelko+": {
@@ -6426,8 +6645,8 @@
["src/views/board/components/List.tsx", 117],
["src/views/board/components/UpdateBoardSlugButton.tsx", 58],
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 487],
["src/views/board/index.tsx", 545],
["src/views/board/index.tsx", 603],
["src/views/board/index.tsx", 661],
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,7 @@ import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";
import { FontSizeProvider } from "~/providers/font-size";
import { KeyboardShortcutProvider } from "~/providers/keyboard-shortcuts";
import { LinguiProviderWrapper } from "~/providers/lingui";
import { ModalProvider } from "~/providers/modal";
@@ -83,19 +84,21 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
<main className="font-sans">
<KeyboardShortcutProvider>
<LinguiProviderWrapper>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ModalProvider>
<PopupProvider>
{posthogKey ? (
<PostHogProvider client={posthog}>
{getLayout(<Component {...pageProps} />)}
</PostHogProvider>
) : (
getLayout(<Component {...pageProps} />)
)}
</PopupProvider>
</ModalProvider>
</ThemeProvider>
<FontSizeProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ModalProvider>
<PopupProvider>
{posthogKey ? (
<PostHogProvider client={posthog}>
{getLayout(<Component {...pageProps} />)}
</PostHogProvider>
) : (
getLayout(<Component {...pageProps} />)
)}
</PopupProvider>
</ModalProvider>
</ThemeProvider>
</FontSizeProvider>
</LinguiProviderWrapper>
</KeyboardShortcutProvider>
</main>

View File

@@ -1,12 +1,13 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { env } from "~/env";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}
@@ -32,7 +33,9 @@ export default withRateLimit(
try {
allowedHost = new URL(s3Endpoint).hostname.toLowerCase();
} catch {
return res.status(500).json({ message: "Storage endpoint misconfigured" });
return res
.status(500)
.json({ message: "Storage endpoint misconfigured" });
}
if (hostname !== allowedHost && !hostname.endsWith(`.${allowedHost}`)) {
@@ -66,10 +69,7 @@ export default withRateLimit(
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" });
return res.status(500).json({ message: "Failed to download attachment" });
}
},
}),
);

View File

@@ -1,25 +1,25 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}
try {
const response = await fetch("https://formbricks.com/api/oss-friends");
if (!response.ok) {
throw new Error("Failed to fetch from Formbricks");
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}
const data = await response.json();
return res.status(200).json(data);
} catch (error) {
console.error("Error fetching OSS friends:", error);
return res.status(500).json({ message: "Failed to fetch OSS friends" });
}
},
try {
const response = await fetch("https://formbricks.com/api/oss-friends");
if (!response.ok) {
throw new Error("Failed to fetch from Formbricks");
}
const data = await response.json();
return res.status(200).json(data);
} catch (error) {
return res.status(500).json({ message: "Failed to fetch OSS friends" });
}
}),
);

View File

@@ -2,34 +2,34 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { env } from "next-runtime-env";
import { createNextApiContext } from "@kan/api/trpc";
import { createStripeClient } from "@kan/stripe";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { createStripeClient } from "@kan/stripe";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
const stripe = createStripeClient();
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
const stripe = createStripeClient();
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user } = await createNextApiContext(req);
if (!user?.stripeCustomerId) {
return res.status(404).json({ error: "No billing account found" });
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${env("NEXT_PUBLIC_BASE_URL")}/settings`,
});
try {
const { user } = await createNextApiContext(req);
return res.status(200).json({ url: session.url });
} catch (error) {
console.error("Error:", error);
return res.status(500).json({ error: "Error creating portal session" });
}
},
if (!user?.stripeCustomerId) {
return res.status(404).json({ error: "No billing account found" });
}
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${env("NEXT_PUBLIC_BASE_URL")}/settings`,
});
return res.status(200).json({ url: session.url });
} catch (error) {
return res.status(500).json({ error: "Error creating portal session" });
}
}),
);

View File

@@ -3,10 +3,13 @@ import { env } from "next-runtime-env";
import { z } from "zod";
import { createNextApiContext } from "@kan/api/trpc";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { assertPermission } from "@kan/api/utils/permissions";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { generateUID } from "@kan/shared/utils";
import { createStripeClient } from "@kan/stripe";
import { withRateLimit } from "@kan/api/utils/rateLimit";
const workspaceSlugSchema = z
.string()
@@ -17,103 +20,135 @@ const workspaceSlugSchema = z
interface CheckoutSessionRequest {
successUrl: string;
cancelUrl: string;
slug: string;
workspacePublicId: string;
stripeCustomerId: string;
billing?: string;
workspacePublicId?: string;
slug?: string;
workspaceName?: string;
workspaceDescription?: string;
workspaceSlug?: string;
plan?: string;
}
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
const stripe = createStripeClient();
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
const stripe = createStripeClient();
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user, db } = await createNextApiContext(req);
if (!user) {
return res.status(404).json({ error: "User not found" });
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const body = req.body as CheckoutSessionRequest;
const { successUrl, cancelUrl, slug, workspacePublicId } = body;
try {
const { user, db } = await createNextApiContext(req);
if (!successUrl || !cancelUrl || !workspacePublicId) {
return res.status(400).json({ error: "Missing required fields" });
}
if (slug) {
const slugResult = workspaceSlugSchema.safeParse(slug);
if (!slugResult.success) {
return new Response(
JSON.stringify({ error: "Invalid workspace slug" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
}
const workspace = await workspaceRepo.getAllByUserId(db, user.id);
const isMemberOfWorkspace = workspace.some(
({ workspace }) => workspace.publicId === body.workspacePublicId,
);
if (!isMemberOfWorkspace) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}
const subscription = await subscriptionRepo.create(db, {
plan: "pro",
referenceId: workspacePublicId,
userId: user.id,
stripeCustomerId: user.stripeCustomerId ?? "",
status: "incomplete",
});
const subscriptionId = subscription?.id;
if (!subscriptionId) {
return res.status(500).json({ error: "Error creating subscription" });
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_collection: "always",
line_items: [
{
price: process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID,
quantity: 1,
},
],
subscription_data: {
trial_period_days: 14,
},
success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}`,
cancel_url: `${env("NEXT_PUBLIC_BASE_URL")}${cancelUrl}`,
client_reference_id: workspacePublicId,
customer: user.stripeCustomerId ?? undefined,
metadata: {
...(slug && { workspaceSlug: slug }),
const body = req.body as CheckoutSessionRequest;
const {
successUrl,
cancelUrl,
billing,
workspacePublicId,
userId: user.id,
subscriptionId,
},
});
slug,
workspaceName,
workspaceDescription,
workspaceSlug,
} = body;
return res.status(200).json({ url: session.url });
} catch (error) {
console.error("Error:", error);
return res.status(500).json({ error: "Error creating checkout session" });
}
},
if (!successUrl || !cancelUrl) {
return res.status(400).json({ error: "Missing required fields" });
}
if (!workspacePublicId && !workspaceName) {
return res
.status(400)
.json({ error: "Must provide workspacePublicId or workspaceName" });
}
const resolvedSlug = slug ?? workspaceSlug;
if (resolvedSlug) {
const slugResult = workspaceSlugSchema.safeParse(resolvedSlug);
if (!slugResult.success) {
return res.status(400).json({ error: "Invalid workspace slug" });
}
}
let resolvedWorkspacePublicId = workspacePublicId;
let subscriptionId: number | undefined;
if (workspacePublicId) {
// Existing workspace upgrade
const workspace = await workspaceRepo.getByPublicId(
db,
workspacePublicId,
);
if (!workspace) {
return res.status(404).json({ error: "Workspace not found" });
}
try {
await assertPermission(db, user.id, workspace.id, "workspace:manage");
} catch {
return res.status(403).json({ error: "Unauthorized" });
}
const subscription = await subscriptionRepo.create(db, {
plan: "pro",
referenceId: workspacePublicId,
userId: user.id,
stripeCustomerId: user.stripeCustomerId ?? "",
status: "incomplete",
});
subscriptionId = subscription?.id;
if (!subscriptionId) {
return res.status(500).json({ error: "Error creating subscription" });
}
} else {
resolvedWorkspacePublicId = generateUID();
}
const isTeam = body.plan === "team";
const annualPriceId = isTeam
? (process.env.STRIPE_TEAM_PLAN_ANNUAL_PRICE_ID ??
process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID)
: (process.env.STRIPE_PRO_PLAN_ANNUAL_PRICE_ID ??
process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID);
const monthlyPriceId = isTeam
? process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID
: process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID;
const priceId = billing === "annual" ? annualPriceId : monthlyPriceId;
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_collection: "always",
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: { trial_period_days: 14 },
success_url: `${env("NEXT_PUBLIC_BASE_URL")}${successUrl}?workspacePublicId=${resolvedWorkspacePublicId}`,
cancel_url: `${env("NEXT_PUBLIC_BASE_URL")}${cancelUrl}`,
client_reference_id: resolvedWorkspacePublicId,
customer: user.stripeCustomerId ?? undefined,
metadata: {
workspacePublicId: resolvedWorkspacePublicId!,
userId: user.id,
userEmail: user.email ?? "",
...(resolvedSlug && { workspaceSlug: resolvedSlug }),
...(workspaceName && { workspaceName }),
...(workspaceDescription && { workspaceDescription }),
...(subscriptionId && { subscriptionId: String(subscriptionId) }),
plan: body.plan ?? "pro",
isNewWorkspace: workspaceName ? "true" : "false",
},
});
return res.status(200).json({ url: session.url });
} catch (error) {
return res.status(500).json({ error: "Error creating checkout session" });
}
}),
);

View File

@@ -1,10 +1,15 @@
import { randomUUID } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import type { Readable } from "node:stream";
import { createNextApiContext } from "@kan/api/trpc";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createLogger } from "@kan/logger";
import { createStripeClient } from "@kan/stripe";
const log = createLogger("api");
async function buffer(readable: Readable) {
const chunks = [];
for await (const chunk of readable) {
@@ -18,6 +23,9 @@ export default async function handler(
res: NextApiResponse,
) {
const stripe = createStripeClient();
const start = Date.now();
const requestId = randomUUID();
const procedure = req.url?.split("?")[0] ?? "/api/stripe/webhook";
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
@@ -44,24 +52,82 @@ export default async function handler(
switch (event.type) {
case "checkout.session.completed": {
const checkoutSession = event.data.object;
const meta = checkoutSession.metadata;
const metaData = checkoutSession.metadata;
if (!meta?.workspacePublicId) break;
if (metaData?.workspacePublicId) {
await workspaceRepo.update(db, metaData.workspacePublicId, {
...(metaData.workspaceSlug && { slug: metaData.workspaceSlug }),
plan: "pro",
const plan = meta.plan === "team" ? "team" : ("pro" as const);
if (
meta.isNewWorkspace === "true" &&
meta.workspaceName &&
meta.userId &&
meta.userEmail
) {
const existing = await workspaceRepo.getByPublicId(db, meta.workspacePublicId);
if (!existing) {
const slug = meta.workspaceSlug ?? meta.workspacePublicId;
await workspaceRepo.create(db, {
publicId: meta.workspacePublicId,
name: meta.workspaceName,
slug,
plan,
createdBy: meta.userId,
createdByEmail: meta.userEmail,
...(meta.workspaceDescription && {
description: meta.workspaceDescription,
}),
});
await subscriptionRepo.create(db, {
plan,
referenceId: meta.workspacePublicId,
userId: meta.userId,
stripeCustomerId: checkoutSession.customer as string,
status: "active",
});
}
} else {
// Existing workspace upgrade — update plan (and slug for pro)
await workspaceRepo.update(db, meta.workspacePublicId, {
plan,
...(plan === "pro" && meta.workspaceSlug && { slug: meta.workspaceSlug }),
});
}
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
log.warn({ eventType: event.type }, "Unhandled Stripe event type");
}
log.info(
{
requestId,
procedure,
transport: "rest",
duration: Date.now() - start,
status: 200,
input: { eventType: event.type, eventId: event.id },
},
"API OK",
);
return res.status(200).json({ received: true });
} catch (err) {
console.error("Webhook error:", err);
log.error(
{
requestId,
procedure,
transport: "rest",
duration: Date.now() - start,
status: 400,
err,
},
"API error",
);
return res.status(400).json({ message: "Webhook handler failed" });
}
}

View File

@@ -1,52 +1,58 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { addYears } from "date-fns";
import { createNextApiContext } from "@kan/api/trpc";
import { integrations } from "@kan/db/schema";
import { addYears } from "date-fns";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { integrations } from "@kan/db/schema";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const { user } = await createNextApiContext(req);
const { user } = await createNextApiContext(req);
if (!user)
return res.status(401).json({ message: "User not authenticated" });
if (!user)
return res.status(401).json({ message: "User not authenticated" });
const apiKey = process.env.TRELLO_APP_API_KEY;
const apiKey = process.env.TRELLO_APP_API_KEY;
if (!apiKey)
return res.status(500).json({ message: "Trello API key not set in Environment Variables" });
if (!apiKey)
return res
.status(500)
.json({ message: "Trello API key not set in Environment Variables" });
const token = req.body.token;
const token = req.body.token;
if (!token)
return res.status(400).json({ message: "No token found" });
if (!token) return res.status(400).json({ message: "No token found" });
try {
const { db } = await createNextApiContext(req);
try {
const { db } = await createNextApiContext(req);
await db.insert(integrations).values({
provider: "trello",
userId: user.id,
accessToken: token,
expiresAt: addYears(new Date(), 1),
}).onConflictDoUpdate({
set: {
accessToken: token,
expiresAt: addYears(new Date(), 1),
},
target: [integrations.userId, integrations.provider],
});
await db
.insert(integrations)
.values({
provider: "trello",
userId: user.id,
accessToken: token,
expiresAt: addYears(new Date(), 1),
})
.onConflictDoUpdate({
set: {
accessToken: token,
expiresAt: addYears(new Date(), 1),
},
target: [integrations.userId, integrations.provider],
});
return res.status(200).json({ message: "Trello authentication successful" });
} catch (err) {
console.error("Trello authentication error:", err);
return res.status(400).json({ message: "Trello authentication failed" });
}
},
);
return res
.status(200)
.json({ message: "Trello authentication successful" });
} catch (err) {
return res.status(400).json({ message: "Trello authentication failed" });
}
}),
);

View File

@@ -3,9 +3,11 @@ import { Novu } from "@novu/api";
import { jwtVerify } from "jose";
import { z } from "zod";
import { env } from "~/env";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { env } from "~/env";
const requestSchema = z.object({
token: z.string().min(1),
});
@@ -22,84 +24,85 @@ const textEncoder = new TextEncoder();
export default withRateLimit(
{ points: 100, duration: 60 },
async (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",
});
}
withApiLogging(
async (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",
});
}
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);
const parsedBody = requestSchema.safeParse(req.body);
if (!parsedBody.success) {
return res.status(400).json({
success: false,
error: "Invalid request payload.",
code: "BAD_REQUEST",
});
}
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",
});
}
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>;
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",
});
}
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 });
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",
});
}
try {
await novu.subscribers.preferences.update(
{
channels: {
email: false,
},
},
payload.subscriberId,
);
} catch (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 });
},
return res.status(200).json({ success: true });
},
),
);

View File

@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { Upload } from "@aws-sdk/lib-storage";
import { createNextApiContext } from "@kan/api/trpc";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { assertPermission } from "@kan/api/utils/permissions";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import * as cardRepo from "@kan/db/repository/card.repo";
@@ -22,7 +23,7 @@ export const config = {
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
@@ -36,7 +37,9 @@ export default withRateLimit(
const bucket = env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
if (!bucket) {
return res.status(500).json({ error: "Attachments bucket not configured" });
return res
.status(500)
.json({ error: "Attachments bucket not configured" });
}
const cardPublicId = req.query.cardPublicId;
@@ -55,7 +58,9 @@ export default withRateLimit(
}
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return res.status(400).json({ error: "Missing or invalid content length" });
return res
.status(400)
.json({ error: "Missing or invalid content length" });
}
if (contentLength > MAX_SIZE_BYTES) {
@@ -129,8 +134,7 @@ export default withRateLimit(
return res.status(200).json({ attachment });
} catch (error) {
console.error("Attachment upload failed", error);
return res.status(500).json({ error: "Internal server error" });
}
},
}),
);

View File

@@ -2,13 +2,17 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { createNextApiContext } from "@kan/api/trpc";
import * as userRepo from "@kan/db/repository/user.repo";
import { env } from "~/env";
import { withApiLogging } from "@kan/api/utils/apiLogging";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import * as userRepo from "@kan/db/repository/user.repo";
import { createS3Client } from "@kan/shared/utils";
const MAX_SIZE_BYTES = parseInt(process.env.S3_AVATAR_UPLOAD_LIMIT || '2097152', 10); // Default 2MB
import { env } from "~/env";
const MAX_SIZE_BYTES = parseInt(
process.env.S3_AVATAR_UPLOAD_LIMIT || "2097152",
10,
); // Default 2MB
const allowedContentTypes = ["image/jpeg", "image/png", "image/webp"];
export const config = {
@@ -19,7 +23,7 @@ export const config = {
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
withApiLogging(async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
@@ -51,7 +55,9 @@ export default withRateLimit(
}
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return res.status(400).json({ error: "Missing or invalid content length" });
return res
.status(400)
.json({ error: "Missing or invalid content length" });
}
if (contentLength > MAX_SIZE_BYTES) {
@@ -93,9 +99,7 @@ export default withRateLimit(
user: updatedUser,
});
} catch (error) {
console.error("Avatar upload failed", error);
return res.status(500).json({ error: "Internal server error" });
}
},
}),
);

View File

@@ -0,0 +1,31 @@
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { env } from "next-runtime-env";
import { authClient } from "@kan/auth/client";
import { PageHead } from "~/components/PageHead";
import SelectPlanView from "~/views/onboarding/select-plan";
export default function SelectPlanPage() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/login");
}
if (!isPending && env("NEXT_PUBLIC_KAN_ENV") !== "cloud") {
router.push("/boards");
}
}, [session, isPending, router]);
if (isPending || !session?.user) return null;
return (
<>
<PageHead title="Select plan | kan.bn" />
<SelectPlanView />
</>
);
}

View File

@@ -0,0 +1,28 @@
import { useRouter } from "next/navigation";
import { env } from "next-runtime-env";
import { useEffect } from "react";
import { authClient } from "@kan/auth/client";
import { PageHead } from "~/components/PageHead";
import WorkspaceDetailsView from "~/views/onboarding/workspace-details";
export default function WorkspaceDetailsPage() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
useEffect(() => {
if (!isPending && !session?.user) router.push("/login");
if (!isPending && env("NEXT_PUBLIC_KAN_ENV") !== "cloud")
router.push("/boards");
}, [session, isPending, router]);
if (isPending || !session?.user) return null;
return (
<>
<PageHead title="Create workspace | kan.bn" />
<WorkspaceDetailsView />
</>
);
}

View File

@@ -1,24 +1,25 @@
import { useEffect } from "react";
export default function TrelloAuthorize() {
useEffect(() => {
const hash = window.location.hash;
const token = hash.split("=")[1];
if (token) {
console.log("Posting token to /api/trello/authenticate", token);
fetch("/api/trello/authenticate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
}).then(() => {
window.close();
});
}
}, []);
useEffect(() => {
const hash = window.location.hash;
const token = hash.split("=")[1];
if (token) {
fetch("/api/trello/authenticate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
}).then(() => {
window.close();
});
}
}, []);
return <div className="flex h-[200px] items-center justify-center">
<p className="text-center">Connecting to Trello...</p>
</div>;
}
return (
<div className="flex h-[200px] items-center justify-center">
<p className="text-center">Connecting to Trello...</p>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import React, {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
export type FontSize = "small" | "medium" | "large";
const fontSizeMap: Record<FontSize, string> = {
small: "14px",
medium: "16px",
large: "18px",
};
interface FontSizeContextProps {
fontSize: FontSize;
setFontSize: (size: FontSize) => void;
}
const FontSizeContext = createContext<FontSizeContextProps | undefined>(
undefined,
);
export const FontSizeProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [fontSize, setFontSizeState] = useState<FontSize>("medium");
const setFontSize = (size: FontSize) => {
document.documentElement.style.fontSize = fontSizeMap[size];
localStorage.setItem("fontSize", size);
setFontSizeState(size);
};
useEffect(() => {
const stored = localStorage.getItem("fontSize") as FontSize | null;
const size = stored && fontSizeMap[stored] ? stored : "medium";
document.documentElement.style.fontSize = fontSizeMap[size];
setFontSizeState(size);
}, []);
return (
<FontSizeContext.Provider value={{ fontSize, setFontSize }}>
{children}
</FontSizeContext.Provider>
);
};
export const useFontSize = (): FontSizeContextProps => {
const context = useContext(FontSizeContext);
if (!context) {
throw new Error("useFontSize must be used within a FontSizeProvider");
}
return context;
};

View File

@@ -17,7 +17,7 @@ interface Workspace {
description: string | null | undefined;
publicId: string;
slug: string | undefined;
plan: "free" | "pro" | "enterprise" | undefined;
plan: "free" | "team" | "pro" | "enterprise" | undefined;
role: "admin" | "member" | "guest";
weekStartDay: 0 | 1 | 6;
}
@@ -27,7 +27,7 @@ const initialWorkspace: Workspace = {
description: null,
publicId: "",
slug: "",
plan: "free",
plan: "free" as const,
role: "member",
weekStartDay: 1,
};
@@ -50,7 +50,15 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
const workspacePublicId = useSearchParams().get("workspacePublicId");
const { data, isLoading } = api.workspace.all.useQuery();
const [pendingWorkspaceId, setPendingWorkspaceId] = useState<string | null>(
workspacePublicId,
);
const pollAttemptsRef = React.useRef(0);
const MAX_POLL_ATTEMPTS = 5;
const { data, isLoading } = api.workspace.all.useQuery(undefined, {
refetchInterval: pendingWorkspaceId ? 2000 : false,
});
const utils = api.useUtils();
const switchWorkspace = (_workspace: Workspace) => {
@@ -94,7 +102,16 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
({ workspace }) => workspace.publicId === storedWorkspaceId,
);
if (!selectedWorkspace?.workspace) return;
if (!selectedWorkspace?.workspace) {
pollAttemptsRef.current += 1;
if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
setPendingWorkspaceId(null);
}
return;
}
pollAttemptsRef.current = 0;
setPendingWorkspaceId(null);
setWorkspace({
publicId: selectedWorkspace.workspace.publicId,

View File

@@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;
html {
font-size: 14px;
}
[contenteditable="true"]:empty:before {
content: attr(placeholder);
display: block;

View File

@@ -1,3 +1,5 @@
import { env } from "next-runtime-env";
export const formatToArray = (
value: string | string[] | undefined,
): string[] => {
@@ -50,5 +52,21 @@ export const getAvatarUrl = (imageOrKey: string | null) => {
return imageOrKey;
}
// Construct URL from S3 key
const useVirtualHosted = env("NEXT_PUBLIC_USE_VIRTUAL_HOSTED_URLS") === "true";
const storageDomain = env("NEXT_PUBLIC_STORAGE_DOMAIN");
const storageUrl = env("NEXT_PUBLIC_STORAGE_URL");
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
if (useVirtualHosted && storageDomain && bucket) {
// Virtual-hosted style: https://{bucket}.{domain}/{key}
return `https://${bucket}.${storageDomain}/${imageOrKey}`;
}
if (storageUrl && bucket) {
// Path-style: {storageUrl}/{bucket}/{key}
return `${storageUrl}/${bucket}/${imageOrKey}`;
}
return "";
};

View File

@@ -0,0 +1,41 @@
import { t } from "@lingui/core/macro";
import { useModal } from "~/providers/modal";
import { api } from "~/utils/api";
import { DueDateSelector } from "~/views/card/components/DueDateSelector";
export function CardContextDueDateModal() {
const { entityId: cardPublicId, closeModal } = useModal();
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardPublicId ?? "" },
{ enabled: !!cardPublicId && cardPublicId.length >= 12 },
);
if (!cardPublicId) return null;
return (
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-light-1000 dark:text-dark-1000">
{t`Set due date`}
</h2>
{isLoading ? (
<div className="h-10 w-full animate-pulse rounded bg-light-200 dark:bg-dark-300" />
) : (
<DueDateSelector
cardPublicId={cardPublicId}
dueDate={card?.dueDate ?? null}
/>
)}
<div className="mt-4 flex justify-end">
<button
type="button"
onClick={closeModal}
className="rounded-md border border-light-300 bg-light-50 px-3 py-1.5 text-sm font-medium text-light-1000 hover:bg-light-200 dark:border-dark-400 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300"
>
{t`Done`}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,284 @@
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
Transition,
} from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { useState } from "react";
import { HiChevronDown } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
import Button from "~/components/Button";
import Input from "~/components/Input";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
interface CardContextDuplicateModalProps {
boardPublicId?: string;
isTemplate?: boolean;
}
export function CardContextDuplicateModal({
boardPublicId: boardPublicIdProp,
isTemplate: isTemplateProp,
}: CardContextDuplicateModalProps = {}) {
const { entityId: cardPublicId, closeModal, getModalState } = useModal();
const { showPopup } = usePopup();
const utils = api.useUtils();
const modalState = getModalState("CARD_CONTEXT_DUPLICATE") as
| { boardPublicId: string; isTemplate?: boolean }
| undefined;
const boardPublicId =
boardPublicIdProp ?? modalState?.boardPublicId ?? "";
const isTemplate = isTemplateProp ?? modalState?.isTemplate ?? false;
const [listPublicId, setListPublicId] = useState("");
const [copyLabels, setCopyLabels] = useState(true);
const [copyMembers, setCopyMembers] = useState(true);
const [copyChecklists, setCopyChecklists] = useState(true);
const [position, setPosition] = useState<string>("");
const [title, setTitle] = useState("");
const { data: card, isLoading: isCardLoading } = api.card.byId.useQuery(
{ cardPublicId: cardPublicId ?? "" },
{ enabled: !!cardPublicId && cardPublicId.length >= 12 },
);
const boardType = isTemplate ? "template" : "regular";
const { data: board } = api.board.byId.useQuery(
{ boardPublicId, type: boardType },
{ enabled: !!boardPublicId },
);
const lists = board?.lists ?? [];
const listOptions = lists.map((l) => ({ publicId: l.publicId, name: l.name }));
const currentListPublicId = card?.list?.publicId;
const hasLabels = (card?.labels?.length ?? 0) > 0;
const hasMembers = (card?.members?.length ?? 0) > 0;
const hasChecklists = (card?.checklists?.length ?? 0) > 0;
const hasAnyCopyOption = hasLabels || hasMembers || hasChecklists;
const duplicateCard = api.card.duplicate.useMutation({
onSuccess: () => {
showPopup({
header: t`Card duplicated`,
icon: "success",
message: t`The card has been duplicated.`,
});
closeModal();
},
onError: () => {
showPopup({
header: t`Unable to duplicate card`,
message: t`Please try again.`,
icon: "error",
});
},
onSettled: async () => {
await utils.board.byId.invalidate();
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!cardPublicId || !listPublicId) return;
const indexNum =
position === "" ? undefined : parseInt(position, 10);
if (
position !== "" &&
(indexNum === undefined || isNaN(indexNum) || indexNum < 0)
)
return;
duplicateCard.mutate({
cardPublicId,
listPublicId,
copyLabels,
copyMembers,
copyChecklists,
...(typeof indexNum === "number" && { index: indexNum }),
title: title.trim() || undefined,
});
};
if (!cardPublicId) return null;
return (
<form onSubmit={handleSubmit} className="p-4">
<h2 className="mb-4 text-lg font-semibold text-light-1000 dark:text-dark-1000">
{t`Duplicate card`}
</h2>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-light-900 dark:text-dark-900">
{t`List`}
</label>
<Listbox value={listPublicId} onChange={setListPublicId}>
<div className="relative">
<ListboxButton
className={twMerge(
"relative w-full cursor-pointer rounded-md border border-light-300 bg-white py-2 pl-3 pr-10 text-left text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-light-400 focus:ring-offset-0 dark:border-dark-400 dark:bg-dark-200 dark:text-dark-1000 dark:focus:ring-dark-500",
!listPublicId && "text-light-600 dark:text-dark-600",
)}
>
<span className="block truncate">
{listPublicId
? listOptions.find((o) => o.publicId === listPublicId)
?.name
: t`Select a list`}
</span>
<span className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center">
<HiChevronDown
className="h-4 w-4 text-light-600 dark:text-dark-600"
aria-hidden
/>
</span>
</ListboxButton>
<Transition
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions className="absolute z-50 mt-1 w-full overflow-hidden rounded-md border border-light-200 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-200">
<div className="max-h-60 overflow-y-auto py-1 pr-1 scrollbar scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600">
{listOptions.map((option) => {
const isCurrentList =
option.publicId === currentListPublicId;
return (
<ListboxOption
key={option.publicId}
value={option.publicId}
disabled={isCurrentList}
className={({ focus }) =>
twMerge(
"relative select-none py-2 pl-3 pr-9 text-sm",
isCurrentList
? "cursor-default opacity-50"
: "cursor-pointer",
!isCurrentList && focus
? "bg-light-200 text-light-1000 dark:bg-dark-400 dark:text-dark-1000"
: "text-light-900 dark:text-dark-900",
)
}
>
{option.name}
</ListboxOption>
);
})}
</div>
</ListboxOptions>
</Transition>
</div>
</Listbox>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-light-900 dark:text-dark-900">
{t`Title (optional)`}
</label>
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={card?.title ?? ""}
className="w-full"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-light-900 dark:text-dark-900">
{t`Position in list (0 = first, leave empty for end)`}
</label>
<Input
type="number"
min={0}
value={position}
onChange={(e) => setPosition(e.target.value)}
placeholder={t`End of list`}
className="w-full"
/>
</div>
{hasAnyCopyOption && (
<div className="flex flex-col gap-2">
{hasLabels && (
<div className="flex items-center gap-3">
<label className="relative mt-[2px] inline-flex h-[16px] w-[16px] flex-shrink-0 cursor-pointer items-center justify-center">
<input
type="checkbox"
checked={copyLabels}
onChange={(e) => setCopyLabels(e.target.checked)}
className={twMerge(
"h-[16px] w-[16px] appearance-none rounded-md border border-light-500 bg-transparent outline-none ring-0 checked:bg-blue-600 focus:shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none dark:border-dark-500 dark:hover:border-dark-500",
"cursor-pointer",
)}
/>
</label>
<span className="text-sm text-light-900 dark:text-dark-900">
{t`Copy labels`}
</span>
</div>
)}
{hasMembers && (
<div className="flex items-center gap-3">
<label className="relative mt-[2px] inline-flex h-[16px] w-[16px] flex-shrink-0 cursor-pointer items-center justify-center">
<input
type="checkbox"
checked={copyMembers}
onChange={(e) => setCopyMembers(e.target.checked)}
className={twMerge(
"h-[16px] w-[16px] appearance-none rounded-md border border-light-500 bg-transparent outline-none ring-0 checked:bg-blue-600 focus:shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none dark:border-dark-500 dark:hover:border-dark-500",
"cursor-pointer",
)}
/>
</label>
<span className="text-sm text-light-900 dark:text-dark-900">
{t`Copy members`}
</span>
</div>
)}
{hasChecklists && (
<div className="flex items-center gap-3">
<label className="relative mt-[2px] inline-flex h-[16px] w-[16px] flex-shrink-0 cursor-pointer items-center justify-center">
<input
type="checkbox"
checked={copyChecklists}
onChange={(e) => setCopyChecklists(e.target.checked)}
className={twMerge(
"h-[16px] w-[16px] appearance-none rounded-md border border-light-500 bg-transparent outline-none ring-0 checked:bg-blue-600 focus:shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none dark:border-dark-500 dark:hover:border-dark-500",
"cursor-pointer",
)}
/>
</label>
<span className="text-sm text-light-900 dark:text-dark-900">
{t`Copy checklists`}
</span>
</div>
)}
</div>
)}
</div>
<div className="mt-6 flex justify-end gap-2">
<Button type="button" variant="secondary" onClick={closeModal}>
{t`Cancel`}
</Button>
<Button
type="submit"
variant="primary"
disabled={
!listPublicId ||
duplicateCard.isPending ||
isCardLoading ||
lists.length === 0
}
>
{duplicateCard.isPending ? t`Duplicating…` : t`Duplicate`}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,53 @@
import { t } from "@lingui/core/macro";
import { useModal } from "~/providers/modal";
import { api } from "~/utils/api";
import LabelIcon from "~/components/LabelIcon";
import LabelSelector from "~/views/card/components/LabelSelector";
export function CardContextLabelsModal() {
const { entityId: cardPublicId, closeModal } = useModal();
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardPublicId ?? "" },
{ enabled: !!cardPublicId && cardPublicId.length >= 12 },
);
const boardLabels = card?.list?.board?.labels ?? [];
const selectedLabels = card?.labels ?? [];
const formattedLabels = boardLabels.map((label) => ({
key: label.publicId,
value: label.name,
selected: selectedLabels.some((l) => l.publicId === label.publicId),
leftIcon: <LabelIcon colourCode={label.colourCode} />,
}));
if (!cardPublicId) return null;
return (
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-light-1000 dark:text-dark-1000">
{t`Labels`}
</h2>
{isLoading ? (
<div className="h-10 w-full animate-pulse rounded bg-light-200 dark:bg-dark-300" />
) : (
<LabelSelector
cardPublicId={cardPublicId}
labels={formattedLabels}
isLoading={isLoading}
/>
)}
<div className="mt-4 flex justify-end">
<button
type="button"
onClick={closeModal}
className="rounded-md border border-light-300 bg-light-50 px-3 py-1.5 text-sm font-medium text-light-1000 hover:bg-light-200 dark:border-dark-400 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300"
>
{t`Done`}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
import { t } from "@lingui/core/macro";
import { useModal } from "~/providers/modal";
import { api } from "~/utils/api";
import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers";
import Avatar from "~/components/Avatar";
import MemberSelector from "~/views/card/components/MemberSelector";
export function CardContextMembersModal() {
const { entityId: cardPublicId, closeModal } = useModal();
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardPublicId ?? "" },
{ enabled: !!cardPublicId && cardPublicId.length >= 12 },
);
const board = card?.list?.board;
const workspaceMembers = board?.workspace?.members ?? [];
const selectedMembers = card?.members ?? [];
const formattedMembers = workspaceMembers.map((member) => {
const isSelected = selectedMembers.some(
(m) => m.publicId === member.publicId,
);
return {
key: member.publicId,
value: formatMemberDisplayName(
member.user?.name ?? null,
member.user?.email ?? member.email,
),
imageUrl: member.user?.image ? getAvatarUrl(member.user.image) : undefined,
selected: isSelected,
leftIcon: (
<Avatar
size="xs"
name={member.user?.name ?? ""}
imageUrl={
member.user?.image ? getAvatarUrl(member.user.image) : undefined
}
email={member.user?.email ?? member.email}
/>
),
};
});
if (!cardPublicId) return null;
return (
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-light-1000 dark:text-dark-1000">
{t`Manage members`}
</h2>
{isLoading ? (
<div className="h-10 w-full animate-pulse rounded bg-light-200 dark:bg-dark-300" />
) : (
<MemberSelector
cardPublicId={cardPublicId}
members={formattedMembers}
isLoading={isLoading}
/>
)}
<div className="mt-4 flex justify-end">
<button
type="button"
onClick={closeModal}
className="rounded-md border border-light-300 bg-light-50 px-3 py-1.5 text-sm font-medium text-light-1000 hover:bg-light-200 dark:border-dark-400 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300"
>
{t`Done`}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,130 @@
import { t } from "@lingui/core/macro";
import { useEffect, useRef } from "react";
import {
HiLink,
HiOutlineCalendar,
HiOutlineDocumentDuplicate,
HiOutlineTag,
HiOutlineTrash,
HiOutlineUserGroup,
HiOutlineArrowRightCircle,
} from "react-icons/hi2";
export type CardContextMenuAction =
| "members"
| "move"
| "labels"
| "dueDate"
| "copyLink"
| "duplicate"
| "delete";
interface CardContextMenuProps {
x: number;
y: number;
onClose: () => void;
onAction: (action: CardContextMenuAction) => void;
canEdit: boolean;
}
const MENU_ITEMS: {
action: CardContextMenuAction;
label: string;
icon: React.ReactNode;
requiresEdit: boolean;
}[] = [
{
action: "members",
label: t`Manage members`,
icon: <HiOutlineUserGroup className="h-4 w-4 shrink-0" />,
requiresEdit: true,
},
{
action: "move",
label: t`Move to another list`,
icon: <HiOutlineArrowRightCircle className="h-4 w-4 shrink-0" />,
requiresEdit: true,
},
{
action: "labels",
label: t`Add / edit label`,
icon: <HiOutlineTag className="h-4 w-4 shrink-0" />,
requiresEdit: true,
},
{
action: "dueDate",
label: t`Set due date`,
icon: <HiOutlineCalendar className="h-4 w-4 shrink-0" />,
requiresEdit: true,
},
{
action: "copyLink",
label: t`Copy link to card`,
icon: <HiLink className="h-4 w-4 shrink-0" />,
requiresEdit: false,
},
{
action: "duplicate",
label: t`Duplicate card`,
icon: <HiOutlineDocumentDuplicate className="h-4 w-4 shrink-0" />,
requiresEdit: true,
},
{
action: "delete",
label: t`Delete card`,
icon: <HiOutlineTrash className="h-4 w-4 shrink-0" />,
requiresEdit: true,
},
];
export function CardContextMenu({
x,
y,
onClose,
onAction,
canEdit,
}: CardContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose();
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
};
}, [onClose]);
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
return (
<div
ref={menuRef}
className="fixed z-[200] min-w-[200px] rounded-md border border-light-200 bg-white py-1 shadow-lg dark:border-dark-400 dark:bg-dark-200"
style={{ left: x, top: y }}
>
{items.map(({ action, label, icon }) => (
<button
key={action}
type="button"
onClick={() => {
onAction(action);
onClose();
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
>
{icon}
{label}
</button>
))}
</div>
);
}

View File

@@ -0,0 +1,101 @@
import { t } from "@lingui/core/macro";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import { invalidateCard } from "~/utils/cardInvalidation";
export function CardContextMoveListModal() {
const { entityId: cardPublicId, closeModal } = useModal();
const { showPopup } = usePopup();
const utils = api.useUtils();
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardPublicId ?? "" },
{ enabled: !!cardPublicId && cardPublicId.length >= 12 },
);
const updateCardList = api.card.update.useMutation({
onMutate: async (vars) => {
if (!cardPublicId) return undefined;
await utils.card.byId.cancel();
const previous = utils.card.byId.getData({ cardPublicId });
utils.card.byId.setData({ cardPublicId }, (old) => {
if (!old) return old;
const list = old.list.board?.lists?.find(
(l) => l.publicId === vars.listPublicId,
);
if (!list) return old;
return {
...old,
list: { ...old.list, publicId: list.publicId, name: list.name },
};
});
return { previous };
},
onError: (_err, _vars, ctx) => {
if (cardPublicId && ctx?.previous) {
utils.card.byId.setData({ cardPublicId }, ctx.previous);
}
showPopup({
header: t`Unable to move card`,
message: t`Please try again.`,
icon: "error",
});
},
onSettled: async () => {
if (cardPublicId) {
await invalidateCard(utils, cardPublicId);
await utils.board.byId.invalidate();
}
},
});
const lists = card?.list?.board?.lists ?? [];
const currentListPublicId = card?.list?.publicId;
const handleSelectList = (listPublicId: string) => {
if (listPublicId === currentListPublicId || !cardPublicId) return;
updateCardList.mutate(
{ cardPublicId, listPublicId, index: 0 },
{ onSuccess: closeModal },
);
};
if (!cardPublicId) return null;
return (
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-light-1000 dark:text-dark-1000">
{t`Move to list`}
</h2>
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div
key={i}
className="h-10 w-full animate-pulse rounded bg-light-200 dark:bg-dark-300"
/>
))}
</div>
) : (
<div className="max-h-[60vh] overflow-y-auto pr-1 scrollbar scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600">
<ul className="space-y-1">
{lists.map((list) => (
<li key={list.publicId}>
<button
type="button"
onClick={() => handleSelectList(list.publicId)}
disabled={list.publicId === currentListPublicId}
className="w-full rounded-md px-3 py-2 text-left text-sm hover:bg-light-200 disabled:opacity-50 dark:hover:bg-dark-400"
>
{list.name}
</button>
</li>
))}
</ul>
</div>
)}
</div>
);
}

View File

@@ -15,6 +15,7 @@ import {
import type { UpdateBoardInput } from "@kan/api/types";
import type { CardContextMenuAction } from "./components/CardContextMenu";
import Button from "~/components/Button";
import { DeleteLabelConfirmation } from "~/components/DeleteLabelConfirmation";
import { LabelForm } from "~/components/LabelForm";
@@ -26,16 +27,23 @@ import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppab
import { Tooltip } from "~/components/Tooltip";
import { EditYouTubeModal } from "~/components/YouTubeEmbed/EditYouTubeModal";
import { useDragToScroll } from "~/hooks/useDragToScroll";
import { useScrollRestore } from "~/hooks/useScrollRestore";
import { usePermissions } from "~/hooks/usePermissions";
import { useScrollRestore } from "~/hooks/useScrollRestore";
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import { formatToArray } from "~/utils/helpers";
import { DeleteCardConfirmation } from "~/views/card/components/DeleteCardConfirmation";
import BoardDropdown from "./components/BoardDropdown";
import Card from "./components/Card";
import { CardContextDueDateModal } from "./components/CardContextDueDateModal";
import { CardContextDuplicateModal } from "./components/CardContextDuplicateModal";
import { CardContextLabelsModal } from "./components/CardContextLabelsModal";
import { CardContextMembersModal } from "./components/CardContextMembersModal";
import { CardContextMenu } from "./components/CardContextMenu";
import { CardContextMoveListModal } from "./components/CardContextMoveListModal";
import { DeleteBoardConfirmation } from "./components/DeleteBoardConfirmation";
import { DeleteListConfirmation } from "./components/DeleteListConfirmation";
import Filters from "./components/Filters";
@@ -55,11 +63,18 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
const utils = api.useUtils();
const { showPopup } = usePopup();
const { workspace } = useWorkspace();
const { openModal, modalContentType, entityId, isOpen } = useModal();
const { openModal, modalContentType, entityId, isOpen, setModalState } =
useModal();
const [selectedPublicListId, setSelectedPublicListId] =
useState<PublicListId>("");
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [contextMenu, setContextMenu] = useState<{
x: number;
y: number;
cardPublicId: string;
} | null>(null);
const { ref: scrollRef, onMouseDown } = useDragToScroll({
enabled: true,
direction: "horizontal",
@@ -134,7 +149,10 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
// Redirect to 404 if board doesn't exist
useEffect(() => {
if (router.isReady && boardId && !isQueryLoading) {
if (error?.data?.code === "NOT_FOUND" || (!boardData && !isQueryLoading)) {
if (
error?.data?.code === "NOT_FOUND" ||
(!boardData && !isQueryLoading)
) {
router.replace("/404");
}
}
@@ -152,7 +170,12 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
const isLoading = isInitialLoading || isQueryLoading;
useScrollRestore(boardId, scrollRef, router, !isLoading && (boardData?.lists.length ?? 0) > 0);
useScrollRestore(
boardId,
scrollRef,
router,
!isLoading && (boardData?.lists.length ?? 0) > 0,
);
const updateListMutation = api.list.update.useMutation({
onMutate: async (args) => {
@@ -267,6 +290,56 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
setSelectedPublicListId(publicBoardId);
};
const handleCardContextMenuAction = (action: CardContextMenuAction) => {
const cardPublicId = contextMenu?.cardPublicId;
if (!cardPublicId) return;
setContextMenu(null);
if (action === "copyLink") {
const path = isTemplate
? `/templates/${boardId}/cards/${cardPublicId}`
: `/cards/${cardPublicId}`;
const url = `${typeof window !== "undefined" ? window.location.origin : ""}${path}`;
void navigator.clipboard.writeText(url).then(
() => {
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Card URL copied to clipboard`,
});
},
() => {
showPopup({
header: t`Unable to copy link`,
icon: "error",
message: t`Please try again.`,
});
},
);
return;
}
if (action === "duplicate") {
setModalState("CARD_CONTEXT_DUPLICATE", {
boardPublicId: boardId ?? "",
isTemplate: !!isTemplate,
});
openModal("CARD_CONTEXT_DUPLICATE", cardPublicId);
return;
}
if (action === "delete") {
openModal("DELETE_CARD", cardPublicId);
return;
}
const modalType =
action === "members"
? "CARD_CONTEXT_MEMBERS"
: action === "move"
? "CARD_CONTEXT_MOVE_LIST"
: action === "labels"
? "CARD_CONTEXT_LABELS"
: "CARD_CONTEXT_DUE_DATE";
openModal(modalType, cardPublicId);
};
const onDragEnd = ({
source: _source,
destination,
@@ -403,6 +476,49 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
>
<EditYouTubeModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "CARD_CONTEXT_MEMBERS"}
>
<CardContextMembersModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "CARD_CONTEXT_MOVE_LIST"}
>
<CardContextMoveListModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "CARD_CONTEXT_LABELS"}
>
<CardContextLabelsModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "CARD_CONTEXT_DUE_DATE"}
>
<CardContextDueDateModal />
</Modal>
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "CARD_CONTEXT_DUPLICATE"}
>
<CardContextDuplicateModal
boardPublicId={boardId ?? ""}
isTemplate={!!isTemplate}
/>
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_CARD"}
>
<DeleteCardConfirmation
cardPublicId={entityId}
boardPublicId={boardId ?? ""}
/>
</Modal>
</>
);
};
@@ -605,18 +721,33 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
)
e.preventDefault();
}}
onContextMenu={(e) => {
if (
card.publicId.startsWith(
"PLACEHOLDER",
)
)
return;
e.preventDefault();
setContextMenu({
x: e.clientX,
y: e.clientY,
cardPublicId: card.publicId,
});
}}
key={card.publicId}
href={
isTemplate
? `/templates/${boardId}/cards/${card.publicId}`
: `/cards/${card.publicId}`
}
className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
"PLACEHOLDER",
)
? "pointer-events-none"
: ""
}`}
className={`mb-2 flex !cursor-pointer flex-col ${
card.publicId.startsWith(
"PLACEHOLDER",
)
? "pointer-events-none"
: ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
@@ -653,6 +784,15 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
</>
) : null}
</div>
{contextMenu && (
<CardContextMenu
x={contextMenu.x}
y={contextMenu.y}
onClose={() => setContextMenu(null)}
onAction={handleCardContextMenuAction}
canEdit={!!canEditCard}
/>
)}
{renderModalContent()}
</div>
</>

View File

@@ -234,7 +234,7 @@ const ImportGithub: React.FC = () => {
await refetchBoards();
closeModal();
} catch (e) {
console.log(e);
console.error(e);
}
},
onError: () => {
@@ -386,7 +386,7 @@ const ImportTrello: React.FC = () => {
await refetchBoards();
closeModal();
} catch (e) {
console.log(e);
console.error(e);
}
},
onError: () => {

View File

@@ -1,11 +1,11 @@
import type { DraggableProvided } from "react-beautiful-dnd";
import { t } from "@lingui/core/macro";
import { useEffect, useState } from "react";
import ContentEditable from "react-contenteditable";
import { useState } from "react";
import { HiXMark } from "react-icons/hi2";
import { RiDraggable } from "react-icons/ri";
import { twMerge } from "tailwind-merge";
import PlainTextEditor from "~/components/PlainTextEditor";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import { invalidateCard } from "~/utils/cardInvalidation";
@@ -33,9 +33,7 @@ export default function ChecklistItemRow({
}: ChecklistItemRowProps) {
const utils = api.useUtils();
const { showPopup } = usePopup();
const [title, setTitle] = useState("");
const [completed, setCompleted] = useState(false);
const [completed, setCompleted] = useState(item.completed);
const updateItem = api.checklist.updateItem.useMutation({
onMutate: async (vars) => {
@@ -103,21 +101,6 @@ export default function ChecklistItemRow({
},
});
// Only resync from props when switching items to avoid clobbering edits
useEffect(() => {
setTitle(item.title);
setCompleted(item.completed);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.publicId]);
const sanitizeHtmlToPlainText = (html: string): string =>
html
.replace(/<br\s*\/?>(\n)?/gi, "\n")
.replace(/<div><br\s*\/?><\/div>/gi, "")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/g, " ")
.trim();
const handleToggleCompleted = () => {
if (viewOnly) return;
setCompleted((prev) => !prev);
@@ -127,14 +110,8 @@ export default function ChecklistItemRow({
});
};
const commitTitle = (rawHtml: string) => {
if (viewOnly) return;
const plain = sanitizeHtmlToPlainText(rawHtml);
if (!plain || plain === item.title) {
setTitle(item.title);
return;
}
setTitle(plain);
const commitTitle = (plain: string) => {
if (!plain || plain === item.title) return;
updateItem.mutate({
checklistItemPublicId: item.publicId,
title: plain,
@@ -183,36 +160,26 @@ export default function ChecklistItemRow({
)}
/>
</label>
<div className="flex-1 pr-7">
<ContentEditable
html={title}
disabled={viewOnly}
onChange={(e) => setTitle(e.target.value)}
// @ts-expect-error - valid event
onBlur={(e: Event) => {
const innerHTML = (e.target as HTMLElement).innerHTML;
commitTitle(innerHTML);
<PlainTextEditor
key={item.publicId}
content={item.title}
readOnly={viewOnly}
placeholder={t`Add details...`}
onBlur={commitTitle}
onEnter={(plain) => {
commitTitle(plain);
onCreateNewItem?.();
}}
onEscape={() => undefined}
className={twMerge(
"m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-950 outline-none focus-visible:outline-none dark:text-dark-950",
"m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-950 dark:text-dark-950",
viewOnly && "cursor-default",
)}
placeholder={t`Add details...`}
onKeyDown={(e) => {
if (viewOnly) return;
if (e.key === "Enter") {
e.preventDefault();
const innerHTML = (e.currentTarget as HTMLElement).innerHTML;
commitTitle(innerHTML);
onCreateNewItem?.();
}
if (e.key === "Escape") {
e.preventDefault();
setTitle(item.title);
}
}}
/>
</div>
{!viewOnly && (
<button
type="button"

View File

@@ -78,6 +78,7 @@ export default function LabelSelector({
},
onSettled: async () => {
await invalidateCard(utils, cardPublicId);
await utils.board.byId.invalidate();
},
});

View File

@@ -58,6 +58,7 @@ export default function ListSelector({
},
onSettled: async () => {
await invalidateCard(utils, cardPublicId);
await utils.board.byId.invalidate();
},
});

View File

@@ -85,6 +85,7 @@ export default function MemberSelector({
},
onSettled: async () => {
await invalidateCard(utils, cardPublicId);
await utils.board.byId.invalidate();
},
});

View File

@@ -0,0 +1,289 @@
import { useRouter, useSearchParams } from "next/navigation";
import { t } from "@lingui/core/macro";
import { Radio, RadioGroup } from "@headlessui/react";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
import { HiUser } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import { api } from "~/utils/api";
type PlanId = "solo" | "team" | "pro";
type Billing = "monthly" | "annual";
// Each user orbits via a zero-size pivot div at center (144,144) that rotates.
// The icon is offset from the pivot by `radius` pixels along the x-axis.
// A counter-rotation on the icon keeps it upright.
const ORBIT_USERS: Record<
PlanId,
{ id: string; angle: number; radius: number; duration: number }[]
> = {
solo: [],
team: [
{ id: "t1", angle: 45, radius: 104, duration: 50 },
{ id: "t2", angle: 170, radius: 104, duration: 50 },
{ id: "t3", angle: 290, radius: 104, duration: 50 },
],
pro: [
{ id: "t1", angle: 45, radius: 104, duration: 50 },
{ id: "t2", angle: 170, radius: 104, duration: 50 },
{ id: "t3", angle: 290, radius: 104, duration: 50 },
{ id: "p1", angle: 10, radius: 144, duration: 70 },
{ id: "p2", angle: 120, radius: 144, duration: 70 },
{ id: "p4", angle: 240, radius: 144, duration: 70 },
{ id: "p3", angle: 230, radius: 64, duration: 30 },
],
};
export default function SelectPlanView() {
const router = useRouter();
const searchParams = useSearchParams();
const [selected, setSelected] = useState<PlanId>(
(searchParams.get("plan") as PlanId | null) ?? "solo",
);
const [billing, setBilling] = useState<Billing>(
(searchParams.get("billing") as Billing | null) ?? "annual",
);
const returnUrl = searchParams.get("returnUrl") ?? "/boards";
const { data: workspaces } = api.workspace.all.useQuery();
const { data: session } = authClient.useSession();
const { data: user } = api.user.getUser.useQuery(undefined, {
enabled: !!session?.user,
});
const userImage = user?.image ?? session?.user.image ?? null;
const hasExistingWorkspace = !!workspaces?.length;
const FREQUENCIES: { value: Billing; label: string }[] = [
{ value: "monthly", label: t`Monthly` },
{ value: "annual", label: t`Annual` },
];
const PLANS: {
id: PlanId;
name: string;
monthly: string;
annual: string;
description: string;
trial?: boolean;
}[] = [
{
id: "solo",
name: t`Solo`,
monthly: t`Free`,
annual: t`Free`,
description: t`Good for individuals starting out who just need the essentials.`,
},
{
id: "team",
name: t`Team`,
monthly: "$10/user/mo",
annual: "$8/user/mo",
description: t`Best for small teams who want to collaborate and move faster together.`,
trial: true,
},
{
id: "pro",
name: t`Pro`,
monthly: "$29/mo",
annual: "$23/mo",
description: t`Unlimited members and a custom workspace username for teams ready to scale.`,
trial: true,
},
];
const handleSelectPlan = (plan: PlanId) => {
setSelected(plan);
router.replace(`/onboarding/select-plan?plan=${plan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`);
};
const handleSetBilling = (b: Billing) => {
setBilling(b);
router.replace(`/onboarding/select-plan?plan=${selected}&billing=${b}&returnUrl=${encodeURIComponent(returnUrl)}`);
};
const handleContinue = () =>
router.push(`/onboarding/workspace?plan=${selected}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`);
const handleCancel = () => router.push(returnUrl);
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4 bg-light-100 px-4 py-8 dark:bg-dark-50 md:px-6">
<div className="w-full max-w-3xl overflow-hidden rounded-xl border border-light-400 bg-light-200 shadow-xl dark:border-dark-400 dark:bg-dark-100">
<div className="flex flex-col md:h-[520px] md:flex-row">
{/* Left panel */}
<div className="flex flex-col p-6 md:w-[55%] md:p-8">
<div className="flex-1">
<h2 className="text-xl font-bold text-light-1000 dark:text-dark-1000">
{t`Choose a plan`}
</h2>
<p className="mt-1 text-sm text-light-800 dark:text-dark-800">
{t`Pick a plan to get started. All paid plans include a 14-day free trial.`}
</p>
{/* Billing toggle */}
<div className="mt-4 flex justify-end">
<RadioGroup
value={billing}
onChange={handleSetBilling}
className="grid grid-cols-2 gap-x-1 rounded-full p-1 text-center text-xs font-semibold ring-1 ring-inset ring-light-600 dark:ring-dark-600"
>
{FREQUENCIES.map((f) => (
<Radio
key={f.value}
value={f.value}
className={twMerge(
"cursor-pointer rounded-full px-2.5 py-0.5 text-xs transition-colors",
billing === f.value
? "bg-dark-50 text-white dark:bg-light-50 dark:text-dark-50"
: "text-light-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200",
)}
>
{f.label}
</Radio>
))}
</RadioGroup>
</div>
<div className="mt-4 space-y-3">
{PLANS.map((plan) => {
const badge =
billing === "annual" ? plan.annual : plan.monthly;
return (
<button
key={plan.id}
type="button"
onClick={() => handleSelectPlan(plan.id)}
className={`relative w-full rounded-lg border px-4 py-3 text-left transition-colors ${
selected === plan.id
? "border-light-700 bg-light-300 dark:border-dark-600 dark:bg-dark-200"
: "border-light-500 bg-light-200 hover:border-light-600 dark:border-dark-500 dark:bg-dark-100 dark:hover:border-dark-600"
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1 pr-8">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-light-1000 dark:text-dark-1000">
{plan.name}
</span>
<span className="rounded-full bg-neutral-700 px-2 py-px text-[11px] font-medium text-neutral-200">
{badge}
</span>
{plan.trial && billing === "annual" && (
<span className="rounded-full bg-emerald-500/10 px-2 py-px text-[11px] font-medium text-emerald-600 ring-1 ring-inset ring-emerald-500/20 dark:text-emerald-400">
-20%
</span>
)}
</div>
<p className="mt-1 text-xs text-light-800 dark:text-dark-800">
{plan.description}
</p>
</div>
<div className="mt-0.5 flex-shrink-0">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
selected === plan.id
? "border-light-900 bg-light-900 dark:border-dark-900 dark:bg-dark-900"
: "border-light-700 dark:border-dark-700"
}`}
>
{selected === plan.id && (
<div className="h-1.5 w-1.5 rounded-full bg-light-100 dark:bg-dark-100" />
)}
</div>
</div>
</div>
</button>
);
})}
</div>
</div>
<div className="mt-6 flex justify-end gap-2 md:mt-8">
{hasExistingWorkspace && (
<Button variant="ghost" onClick={handleCancel}>
{t`Cancel`}
</Button>
)}
<Button onClick={handleContinue}>{t`Continue`}</Button>
</div>
</div>
{/* Right panel */}
<div className="hidden items-center justify-center bg-light-200 dark:bg-dark-200 md:flex md:w-[45%]">
<div className="relative h-72 w-72">
{/* Orbit rings */}
<div className="absolute inset-0 m-auto h-72 w-72 rounded-full border border-light-400 dark:border-dark-400" />
<div className="absolute inset-0 m-auto h-52 w-52 rounded-full border border-light-400 dark:border-dark-400" />
<div className="absolute inset-0 m-auto h-32 w-32 rounded-full border border-light-400 dark:border-dark-400" />
{/* Centre person */}
<div className="absolute inset-0 m-auto flex h-16 w-16 items-center justify-center overflow-hidden rounded-full bg-light-300 dark:bg-dark-300">
{userImage ? (
<img
src={userImage}
alt={t`Your avatar`}
className="h-full w-full object-cover"
/>
) : (
<HiUser className="h-7 w-7 text-light-700 dark:text-dark-700" />
)}
</div>
{/* Orbiting users — zero-size pivot at center rotates; icon offset by radius stays on the ring */}
<AnimatePresence>
{ORBIT_USERS[selected].map((u) => (
<motion.div
key={u.id}
style={{ position: "absolute", left: 144, top: 144 }}
initial={{ opacity: 0, rotate: u.angle }}
animate={{ opacity: 1, rotate: u.angle + 360 }}
exit={{ opacity: 0 }}
transition={{
opacity: { duration: 0.3 },
rotate: {
duration: u.duration,
repeat: Infinity,
ease: "linear",
repeatType: "loop",
},
}}
>
{/* Counter-rotate icon so it stays upright */}
<motion.div
style={{
position: "absolute",
left: u.radius - 12,
top: -12,
}}
initial={{ rotate: -u.angle }}
animate={{ rotate: -(u.angle + 360) }}
transition={{
duration: u.duration,
repeat: Infinity,
ease: "linear",
repeatType: "loop",
}}
className="flex h-6 w-6 items-center justify-center rounded-full bg-light-400 dark:bg-dark-400"
>
<HiUser className="h-3.5 w-3.5 text-light-800 dark:text-dark-800" />
</motion.div>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
</div>
</div>
{!hasExistingWorkspace && (
<Button variant="ghost" onClick={() => authClient.signOut()}>
{t`Sign out`}
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1,367 @@
import { useRouter, useSearchParams } from "next/navigation";
import { t } from "@lingui/core/macro";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import {
HiArrowLeft,
HiArrowPath,
HiArrowRight,
HiCheck,
HiEllipsisVertical,
HiInformationCircle,
HiLockClosed,
} from "react-icons/hi2";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import Input from "~/components/Input";
import LoadingSpinner from "~/components/LoadingSpinner";
import Toggle from "~/components/Toggle";
import { Tooltip } from "~/components/Tooltip";
import { useDebounce } from "~/hooks/useDebounce";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
function slugify(value: string) {
return value
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.slice(0, 60);
}
export default function WorkspaceNameView() {
const router = useRouter();
const searchParams = useSearchParams();
const plan = searchParams.get("plan") ?? "solo";
const billing = searchParams.get("billing") ?? "annual";
const returnUrl = searchParams.get("returnUrl") ?? "/boards";
const { showPopup } = usePopup();
const [isProToggle, setIsProToggle] = useState(plan === "pro");
const [name, setName] = useState("");
const [slug, setSlug] = useState("");
const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
const [description, setDescription] = useState("");
const handleNameChange = (value: string) => {
setName(value);
if (isProToggle && !slugManuallyEdited) {
setSlug(slugify(value));
}
};
const [debouncedSlug] = useDebounce(slug, 500);
const isTyping = slug !== debouncedSlug;
const slugAvailability = api.workspace.checkSlugAvailability.useQuery(
{ workspaceSlug: debouncedSlug },
{ enabled: isProToggle && debouncedSlug.length >= 3 && !isTyping },
);
const isSlugAvailable =
slugAvailability.data?.isAvailable && !slugAvailability.data?.isReserved;
const isSlugTaken =
slugAvailability.data?.isAvailable === false &&
!slugAvailability.data?.isReserved;
const isSlugReserved = slugAvailability.data?.isReserved === true;
const slugError = isSlugTaken
? t`This URL has already been taken`
: isSlugReserved
? t`This URL is reserved`
: undefined;
const { data: session } = authClient.useSession();
const { data: user } = api.user.getUser.useQuery(undefined, {
enabled: !!session?.user,
});
const { data: workspaces } = api.workspace.all.useQuery();
const hasExistingWorkspace = !!workspaces?.length;
const utils = api.useUtils();
const previewSlug = isProToggle
? slugify(slug) || slugify(name) || "your-workspace"
: "your-workspace";
const createWorkspace = api.workspace.create.useMutation({
onSuccess: async (workspace) => {
if (!workspace.publicId) return;
localStorage.setItem("workspacePublicId", workspace.publicId);
void utils.workspace.all.invalidate();
router.push("/boards");
},
onError: () => {
showPopup({
header: t`Unable to create workspace`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
});
const [isRedirectingToCheckout, setIsRedirectingToCheckout] = useState(false);
const handleContinue = async () => {
if (!name.trim()) return;
if (plan === "solo") {
createWorkspace.mutate({
name: name.trim(),
...(description.trim() && { description: description.trim() }),
});
return;
}
// team/pro: redirect to Stripe — workspace created on checkout_success
setIsRedirectingToCheckout(true);
try {
const response = await fetch("/api/stripe/create_checkout_session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
workspaceName: name.trim(),
...(description.trim() && {
workspaceDescription: description.trim(),
}),
...(plan === "pro" && slug ? { workspaceSlug: slug } : {}),
cancelUrl: window.location.pathname + window.location.search,
successUrl: "/boards",
billing,
plan,
}),
});
const data = await response.json();
const url = (data as { url: string }).url;
if (url) {
window.location.href = url;
return;
}
} catch {
// fall through
}
setIsRedirectingToCheckout(false);
showPopup({
header: t`Unable to start checkout`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
};
useEffect(() => {
document.getElementById("workspace-name-input")?.focus();
}, []);
const displayName = user?.name ?? session?.user.name ?? "";
const BOARDS = [t`Roadmap`, t`Engineering`, t`Marketing`];
const [visibleCount, setVisibleCount] = useState(1);
useEffect(() => {
const timers = BOARDS.slice(1).map((_, i) =>
setTimeout(() => setVisibleCount(i + 2), (i + 1) * 2000),
);
return () => timers.forEach(clearTimeout);
}, []);
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4 bg-light-100 px-4 py-8 dark:bg-dark-50 md:px-6">
<div className="w-full max-w-3xl overflow-hidden rounded-xl border border-light-400 bg-light-200 shadow-xl dark:border-dark-400 dark:bg-dark-100">
<div className="flex flex-col md:h-[520px] md:flex-row">
{/* Left panel */}
<div className="flex flex-col p-6 md:w-[55%] md:p-8">
<div className="flex-1">
<h2 className="text-xl font-bold text-light-1000 dark:text-dark-1000">
{t`Set up your workspace`}
</h2>
<p className="mt-1 text-sm text-light-800 dark:text-dark-800">
{t`You can always change these later in settings.`}
</p>
<div className="mt-6 space-y-3">
<Input
id="workspace-name-input"
placeholder={t`Workspace name`}
value={name}
onChange={(e) => handleNameChange(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleContinue()}
maxLength={64}
/>
<div className="space-y-2">
<Input
placeholder={t`your-workspace`}
value={isProToggle ? slug : t`your-workspace`}
onChange={(e) => {
setSlugManuallyEdited(true);
setSlug(
e.target.value
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.slice(0, 60),
);
}}
disabled={!isProToggle}
prefix="kan.bn/"
className={
!isProToggle ? "cursor-not-allowed opacity-50" : ""
}
errorMessage={slugError}
iconRight={
!isProToggle ? (
<Tooltip
content={
<span className="text-xs">{t`Custom usernames require upgrading to a Pro plan`}</span>
}
placement="top"
delay={0}
>
<HiInformationCircle className="h-4 w-4 leading-[0] text-dark-700 dark:text-dark-700" />
</Tooltip>
) : isProToggle && slug.length >= 3 ? (
isTyping || slugAvailability.isPending ? (
<LoadingSpinner />
) : isSlugAvailable ? (
<HiCheck className="h-4 w-4 text-white" />
) : null
) : null
}
/>
</div>
{plan !== "pro" && (
<div className="pb-2">
<Toggle
isChecked={isProToggle}
onChange={() => setIsProToggle((v) => !v)}
label={t`Upgrade to Pro ($29/month)`}
labelPosition="after"
/>
</div>
)}
<div>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t`Workspace description`}
maxLength={280}
rows={3}
className="block w-full resize-none rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-sm shadow-sm ring-1 ring-inset ring-light-600 placeholder:text-dark-800 focus:ring-2 focus:ring-inset focus:ring-light-700 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:leading-6"
/>
<p className="mt-1 text-right text-[10px] text-light-700 dark:text-dark-700">
{description.length}/280
</p>
</div>
</div>
</div>
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
<div className="ml-auto flex gap-2">
<Button
variant="ghost"
onClick={() =>
router.replace(
`/onboarding/select-plan?plan=${plan}&billing=${billing}&returnUrl=${encodeURIComponent(returnUrl)}`,
)
}
>
{t`Back`}
</Button>
<Button
onClick={() => void handleContinue()}
disabled={
!name.trim() ||
createWorkspace.isPending ||
isRedirectingToCheckout ||
(isProToggle &&
slug.length >= 3 &&
(isTyping || slugAvailability.isPending || !!slugError))
}
isLoading={
createWorkspace.isPending || isRedirectingToCheckout
}
>
{t`Continue`}
</Button>
</div>
</div>
</div>
{/* Right panel — browser mockup */}
<div className="hidden flex-col bg-light-300 dark:bg-dark-200 md:flex md:w-[45%]">
{/* Browser chrome */}
<div className="flex items-center gap-2 border-b border-light-400 px-3 py-2.5 dark:border-dark-400">
<div className="flex items-center gap-1.5 text-light-700 dark:text-dark-700">
<HiArrowLeft className="h-3.5 w-3.5" />
<HiArrowRight className="h-3.5 w-3.5" />
<HiArrowPath className="h-3.5 w-3.5" />
</div>
<div className="flex flex-1 items-center gap-1.5 rounded-md bg-light-200 px-2.5 py-1 dark:bg-dark-300">
<HiLockClosed className="h-3 w-3 flex-shrink-0 text-light-700 dark:text-dark-700" />
<span className="truncate text-xs text-light-900 dark:text-dark-900">
kan.bn/
<span className="text-light-1000 dark:text-dark-1000">
{previewSlug}
</span>
</span>
</div>
<HiEllipsisVertical className="h-4 w-4 flex-shrink-0 text-light-700 dark:text-dark-700" />
</div>
{/* Browser content */}
<div className="flex flex-1 flex-col items-center overflow-hidden px-14 py-8">
{/* Workspace name + description */}
<p className="text-center text-xs font-bold text-light-1000 dark:text-dark-1000">
{name || t`Your workspace`}
</p>
<p className="mt-0.5 line-clamp-2 break-all text-center text-[10px] text-light-800 dark:text-dark-800">
{description || t`Your workspace description`}
</p>
{/* Boards area */}
<div className="mt-3 flex w-full flex-1 flex-col rounded-lg bg-light-200 p-2 dark:bg-dark-300">
<div className="flex flex-1 flex-col gap-1.5">
{BOARDS.map((board, i) => (
<motion.div
key={board}
initial={{ opacity: 0, y: -12 }}
animate={
i < visibleCount
? { opacity: 1, y: 0 }
: { opacity: 0, y: -12 }
}
transition={{
opacity: { duration: 0.2 },
y: { duration: 0.25, ease: "easeOut" },
}}
className="flex w-full flex-1 items-center justify-center rounded border border-dashed border-light-400 bg-light-50 px-3 text-[10px] font-medium text-light-900 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900"
>
{board}
</motion.div>
))}
</div>
</div>
{/* Footer */}
<p className="mt-2 text-[10px] font-semibold text-light-900 dark:text-dark-900">
kan.bn
</p>
</div>
</div>
</div>
</div>
{!hasExistingWorkspace && (
<Button variant="ghost" onClick={() => authClient.signOut()}>
{t`Sign out`}
</Button>
)}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import { env } from "next-runtime-env";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
import { FontSizeSelector } from "~/components/FontSizeSelector";
import { LanguageSelector } from "~/components/LanguageSelector";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
@@ -54,6 +55,16 @@ export default function AccountSettings() {
<LanguageSelector />
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
{t`Font size`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Change the application font size.`}
</p>
<FontSizeSelector />
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
{t`Delete account`}

View File

@@ -110,18 +110,17 @@ export default function Avatar({
const cropYpx = (crop.y / 100) * image.naturalHeight;
const cropWpx = (crop.width / 100) * image.naturalWidth;
const cropHpx = (crop.height / 100) * image.naturalHeight;
canvas.width = Math.max(1, Math.floor(cropWpx));
canvas.height = Math.max(1, Math.floor(cropHpx));
// Cap output at 512x512 — avatars display at 64x64, so higher res is wasteful
// and causes "File too large" errors on HiDPI screens
const maxSize = 512;
const scale = Math.min(maxSize / cropWpx, maxSize / cropHpx, 1);
canvas.width = Math.max(1, Math.floor(cropWpx * scale));
canvas.height = Math.max(1, Math.floor(cropHpx * scale));
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Canvas not supported");
// For better quality on HiDPI screens
const pixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.width * pixelRatio;
canvas.height = canvas.height * pixelRatio;
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
ctx.imageSmoothingQuality = "high";
ctx.drawImage(
image,
cropXpx,
@@ -130,8 +129,8 @@ export default function Avatar({
cropHpx,
0,
0,
canvas.width / pixelRatio,
canvas.height / pixelRatio,
canvas.width,
canvas.height,
);
const mime = selectedFile?.type ?? "image/jpeg";
@@ -139,6 +138,7 @@ export default function Avatar({
canvas.toBlob(
(b) => (b ? resolve(b) : reject(new Error("toBlob failed"))),
mime,
0.85,
);
});
return blob;

View File

@@ -37,6 +37,11 @@ services:
- NEXT_PUBLIC_USE_STANDALONE_OUTPUT=${NEXT_PUBLIC_USE_STANDALONE_OUTPUT}
- REDIS_URL=${REDIS_URL}
# Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod)
- LOG_LEVEL=${LOG_LEVEL}
- AXIOM_TOKEN=${AXIOM_TOKEN}
- AXIOM_DATASET=${AXIOM_DATASET}
# Stripe
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}

View File

@@ -37,6 +37,9 @@ services:
# Redis (optional - for rate limiting)
- REDIS_URL=${REDIS_URL}
# Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod)
- LOG_LEVEL=${LOG_LEVEL}
# Admin API key (optional)
- KAN_ADMIN_API_KEY=${KAN_ADMIN_API_KEY}

View File

@@ -28,6 +28,10 @@
"types": "./dist/utils/rateLimit.d.ts",
"default": "./src/utils/rateLimit.ts"
},
"./utils/apiLogging": {
"types": "./dist/utils/apiLogging.d.ts",
"default": "./src/utils/apiLogging.ts"
},
"./utils/permissions": {
"types": "./dist/utils/permissions.d.ts",
"default": "./src/utils/permissions.ts"
@@ -48,6 +52,7 @@
"@kan/auth": "workspace:*",
"@kan/db": "workspace:*",
"@kan/email": "workspace:^",
"@kan/logger": "workspace:^",
"@kan/shared": "workspace:^",
"@kan/stripe": "workspace:^",
"@trpc/server": "catalog:",

View File

@@ -227,7 +227,7 @@ export const boardRouter = createTRPCRouter({
boardSlug: z
.string()
.min(3)
.max(24)
.max(60)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
members: z.array(z.string().min(12)).optional(),
labels: z.array(z.string().min(12)).optional(),
@@ -657,7 +657,7 @@ export const boardRouter = createTRPCRouter({
boardSlug: z
.string()
.min(3)
.max(24)
.max(60)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
boardPublicId: z.string().min(12),
}),

View File

@@ -4,6 +4,7 @@ import { z } from "zod";
import * as cardRepo from "@kan/db/repository/card.repo";
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
import * as cardCommentRepo from "@kan/db/repository/cardComment.repo";
import * as checklistRepo from "@kan/db/repository/checklist.repo";
import * as labelRepo from "@kan/db/repository/label.repo";
import * as listRepo from "@kan/db/repository/list.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
@@ -1184,4 +1185,176 @@ export const cardRouter = createTRPCRouter({
return { success: true };
}),
duplicate: protectedProcedure
.meta({
openapi: {
summary: "Duplicate a card",
method: "POST",
path: "/cards/{cardPublicId}/duplicate",
description: "Duplicates a card to a target list with optional options",
tags: ["Cards"],
protect: true,
},
})
.input(
z.object({
cardPublicId: z.string().min(12),
listPublicId: z.string().min(12),
index: z.number().int().min(0).optional(),
title: z.string().min(1).max(2000).optional(),
copyLabels: z.boolean(),
copyMembers: z.boolean(),
copyChecklists: z.boolean(),
}),
)
.output(
z.object({
publicId: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
if (!userId)
throw new TRPCError({
message: `User not authenticated`,
code: "UNAUTHORIZED",
});
const sourceCardMeta = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.cardPublicId,
);
if (!sourceCardMeta)
throw new TRPCError({
message: `Card with public ID ${input.cardPublicId} not found`,
code: "NOT_FOUND",
});
await assertPermission(
ctx.db,
userId,
sourceCardMeta.workspaceId,
"card:create",
);
const targetList = await listRepo.getWorkspaceAndListIdByListPublicId(
ctx.db,
input.listPublicId,
);
if (!targetList)
throw new TRPCError({
message: `List with public ID ${input.listPublicId} not found`,
code: "NOT_FOUND",
});
if (targetList.workspaceId !== sourceCardMeta.workspaceId)
throw new TRPCError({
message: `Target list must be in the same workspace`,
code: "BAD_REQUEST",
});
const sourceCard = await cardRepo.getWithListAndMembersByPublicId(
ctx.db,
input.cardPublicId,
);
if (!sourceCard)
throw new TRPCError({
message: `Card with public ID ${input.cardPublicId} not found`,
code: "NOT_FOUND",
});
const newCard = await cardRepo.create(ctx.db, {
title: input.title ?? sourceCard.title,
description: sourceCard.description ?? "",
createdBy: userId,
listId: targetList.id,
position: "end",
dueDate: sourceCard.dueDate ?? null,
});
if (input.index !== undefined && input.index >= 0) {
await cardRepo.reorder(ctx.db, {
cardId: newCard.id,
newIndex: input.index,
newListId: targetList.id,
});
}
if (input.copyLabels && sourceCard.labels?.length) {
const labelPublicIds = sourceCard.labels.map((l) => l.publicId);
const labels = await labelRepo.getAllByPublicIds(ctx.db, labelPublicIds);
if (labels.length) {
const labelsInsert = labels.map((label) => ({
cardId: newCard.id,
labelId: label.id,
}));
await cardRepo.bulkCreateCardLabelRelationships(ctx.db, labelsInsert);
const cardActivitesInsert = labels.map((cardLabel) => ({
type: "card.updated.label.added" as const,
cardId: newCard.id,
labelId: cardLabel.id,
createdBy: userId,
}));
await cardActivityRepo.bulkCreate(ctx.db, cardActivitesInsert);
}
}
if (input.copyMembers && sourceCard.members?.length) {
const memberPublicIds = sourceCard.members.map((m) => m.publicId);
const members = await workspaceRepo.getAllMembersByPublicIds(
ctx.db,
memberPublicIds,
);
if (members.length) {
const membersInsert = members.map((member) => ({
cardId: newCard.id,
workspaceMemberId: member.id,
}));
await cardRepo.bulkCreateCardWorkspaceMemberRelationships(
ctx.db,
membersInsert,
);
const cardActivitesInsert = members.map((member) => ({
type: "card.updated.member.added" as const,
cardId: newCard.id,
workspaceMemberId: member.id,
createdBy: userId,
}));
await cardActivityRepo.bulkCreate(ctx.db, cardActivitesInsert);
}
}
if (input.copyChecklists && sourceCard.checklists?.length) {
for (const checklist of sourceCard.checklists) {
const newChecklist = await checklistRepo.create(ctx.db, {
cardId: newCard.id,
name: checklist.name,
createdBy: userId,
});
if (!newChecklist?.id) continue;
if (checklist.items?.length) {
for (const item of checklist.items) {
await checklistRepo.createItem(ctx.db, {
checklistId: newChecklist.id,
title: item.title,
createdBy: userId,
completed: item.completed ?? false,
});
}
}
await cardActivityRepo.create(ctx.db, {
type: "card.updated.checklist.added",
cardId: newCard.id,
toTitle: newChecklist.name,
createdBy: userId,
});
}
}
return { publicId: newCard.publicId };
}),
});

View File

@@ -4,6 +4,7 @@ import { z } from "zod";
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 { stripHtml } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { assertPermission } from "../utils/permissions";
@@ -223,7 +224,7 @@ export const checklistRouter = createTRPCRouter({
.input(
z.object({
checklistPublicId: z.string().length(12),
title: z.string().min(1).max(500),
title: z.string().min(1).max(500).transform(stripHtml),
}),
)
.output(checklistItemSchema)
@@ -288,7 +289,7 @@ export const checklistRouter = createTRPCRouter({
.input(
z.object({
checklistItemPublicId: z.string().length(12),
title: z.string().min(1).max(500).optional(),
title: z.string().min(1).max(500).transform(stripHtml).optional(),
completed: z.boolean().optional(),
index: z.number().int().min(0).optional(),
}),
@@ -322,30 +323,29 @@ export const checklistRouter = createTRPCRouter({
const previousTitle = item.title;
let updatedItem;
let updatedItem;
if (input.title !== undefined || input.completed !== undefined) {
updatedItem = await checklistRepo.updateItemById(ctx.db, {
id: item.id,
title: input.title,
completed: input.completed,
});
}
if (input.title !== undefined || input.completed !== undefined) {
updatedItem = await checklistRepo.updateItemById(ctx.db, {
id: item.id,
title: input.title,
completed: input.completed,
});
}
if (input.index !== undefined) {
updatedItem = await checklistRepo.reorderItem(ctx.db, {
itemId: item.id,
newIndex: input.index,
});
}
if (!updatedItem) {
throw new TRPCError({
message: `Failed to update checklist item`,
code: "INTERNAL_SERVER_ERROR",
});
}
if (input.index !== undefined) {
updatedItem = await checklistRepo.reorderItem(ctx.db, {
itemId: item.id,
newIndex: input.index,
});
}
if (!updatedItem) {
throw new TRPCError({
message: `Failed to update checklist item`,
code: "INTERNAL_SERVER_ERROR",
});
}
// Log completion toggle
if (input.completed !== undefined) {
@@ -371,7 +371,6 @@ export const checklistRouter = createTRPCRouter({
}
return updatedItem;
}),
deleteItem: protectedProcedure
.meta({

View File

@@ -19,6 +19,7 @@ import { encryptToken } from "../utils/encryption";
export const integrationRouter = createTRPCRouter({
saveGitHubToken: protectedProcedure
.input(z.object({ token: z.string() }))
.output(z.object({ success: z.boolean() }))
.mutation(async ({ ctx, input }) => {
const user = ctx.user;
@@ -43,7 +44,9 @@ export const integrationRouter = createTRPCRouter({
return { success: true };
}),
disconnectGitHub: protectedProcedure.mutation(async ({ ctx }) => {
disconnectGitHub: protectedProcedure
.output(z.object({ success: z.boolean() }))
.mutation(async ({ ctx }) => {
const user = ctx.user;
if (!user)
@@ -56,7 +59,9 @@ export const integrationRouter = createTRPCRouter({
return { success: true };
}),
getGitHubStatus: protectedProcedure.query(async ({ ctx }) => {
getGitHubStatus: protectedProcedure
.output(z.object({ connected: z.boolean() }))
.query(async ({ ctx }) => {
const user = ctx.user;
if (!user)

View File

@@ -31,7 +31,6 @@ export const userRouter = createTRPCRouter({
.object({
id: z.number(),
prefix: z.string().nullable(),
key: z.string(),
})
.nullable(),
}),
@@ -62,7 +61,7 @@ export const userRouter = createTRPCRouter({
return {
...result,
image: imageUrl,
apiKey: apiKey ?? null,
apiKey: apiKey ? { id: apiKey.id, prefix: apiKey.prefix } : null,
};
}),
update: protectedProcedure

View File

@@ -202,6 +202,7 @@ export const workspaceRouter = createTRPCRouter({
.input(
z.object({
name: z.string().min(1).max(64),
description: z.string().max(280).optional(),
slug: z
.string()
.min(3)
@@ -260,6 +261,7 @@ export const workspaceRouter = createTRPCRouter({
slug: workspaceSlug,
createdBy: userId,
createdByEmail: userEmail,
...(input.description && { description: input.description }),
});
if (!result.publicId)

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { NextApiRequest } from "next";
import type { OpenApiMeta } from "trpc-to-openapi";
@@ -9,6 +10,30 @@ import { ZodError } from "zod";
import type { dbClient } from "@kan/db/client";
import { initAuth } from "@kan/auth/server";
import { createDrizzleClient } from "@kan/db/client";
import { createLogger } from "@kan/logger";
const log = createLogger("api");
const TRPC_STATUS_MAP: Partial<Record<TRPCError["code"], number>> = {
PARSE_ERROR: 400,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_SUPPORTED: 405,
TIMEOUT: 408,
CONFLICT: 409,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
UNPROCESSABLE_CONTENT: 422,
TOO_MANY_REQUESTS: 429,
CLIENT_CLOSED_REQUEST: 499,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
};
export interface User {
id: string;
@@ -47,6 +72,7 @@ interface CreateContextOptions {
db: dbClient;
auth: ReturnType<typeof createAuthWithHeaders>;
headers: Headers;
transport?: "trpc" | "rest";
}
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
@@ -55,6 +81,8 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
db: opts.db,
auth: opts.auth,
headers: opts.headers,
transport: opts.transport ?? "trpc",
requestId: randomUUID(),
};
};
@@ -66,7 +94,13 @@ export const createTRPCContext = async ({ req }: CreateNextContextOptions) => {
const session = await auth.api.getSession();
return createInnerTRPCContext({ db, user: session?.user, auth, headers });
return createInnerTRPCContext({
db,
user: session?.user,
auth,
headers,
transport: "trpc",
});
};
export const createNextApiContext = async (req: NextApiRequest) => {
@@ -77,7 +111,13 @@ export const createNextApiContext = async (req: NextApiRequest) => {
const session = await auth.api.getSession();
return createInnerTRPCContext({ db, user: session?.user, auth, headers });
return createInnerTRPCContext({
db,
user: session?.user,
auth,
headers,
transport: "trpc",
});
};
export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
@@ -90,11 +130,16 @@ export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
try {
session = await auth.api.getSession();
} catch (error) {
console.error("Error getting session, ", error);
throw error;
log.warn({ err: error }, "Failed to get session, treating as unauthenticated");
}
return createInnerTRPCContext({ db, user: session?.user, auth, headers });
return createInnerTRPCContext({
db,
user: session?.user,
auth,
headers,
transport: "rest",
});
};
const t = initTRPC
@@ -118,10 +163,46 @@ export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const publicProcedure = t.procedure.meta({
openapi: { method: "GET", path: "/public" },
const loggingMiddleware = t.middleware(async ({ path, type, next, ctx, getRawInput }) => {
const start = Date.now();
const [result, input] = await Promise.all([next(), getRawInput().catch(() => undefined)]);
const duration = Date.now() - start;
const { user, transport, requestId } = ctx as {
user?: { id: string; email: string };
transport?: string;
requestId?: string;
};
const isCloud = process.env.NEXT_PUBLIC_KAN_ENV === "cloud";
const meta = {
requestId,
procedure: path,
type,
transport,
duration,
userId: user?.id,
...(isCloud && { email: user?.email }),
input,
};
const label = transport === "rest" ? "REST" : "tRPC";
if (result.ok) {
log.info({ ...meta, status: 200 }, `${label} OK`);
} else {
const status = TRPC_STATUS_MAP[result.error.code] ?? 500;
const errorCode = result.error.code;
log.error(
{ ...meta, status, errorCode, err: result.error },
`${label} error`,
);
}
return result;
});
export const publicProcedure = t.procedure.use(loggingMiddleware);
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
@@ -142,14 +223,12 @@ const enforceUserIsAdmin = t.middleware(async ({ ctx, next }) => {
});
});
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed).meta({
openapi: {
method: "GET",
path: "/protected",
},
});
export const protectedProcedure = t.procedure
.use(loggingMiddleware)
.use(enforceUserIsAuthed);
export const adminProtectedProcedure = t.procedure
.use(loggingMiddleware)
.use(enforceUserIsAdmin)
.meta({
openapi: {

View File

@@ -0,0 +1,64 @@
import { randomUUID } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { createLogger } from "@kan/logger";
import { createNextApiContext } from "../trpc";
const log = createLogger("api");
const isCloud = process.env.NEXT_PUBLIC_KAN_ENV === "cloud";
export function withApiLogging(
handler: (
req: NextApiRequest,
res: NextApiResponse,
) => Promise<unknown> | unknown,
) {
return async (req: NextApiRequest, res: NextApiResponse) => {
const start = Date.now();
const requestId = randomUUID();
const route = req.url?.split("?")[0] ?? "unknown";
const input = {
...(req.query && Object.keys(req.query).length > 0 && { query: req.query }),
...(req.body && typeof req.body === "object" && Object.keys(req.body).length > 0 && { body: req.body }),
};
let statusCode = 200;
const originalStatus = res.status.bind(res);
res.status = (code: number) => {
statusCode = code;
return originalStatus(code);
};
let userId: string | undefined;
let email: string | undefined;
try {
const ctx = await createNextApiContext(req);
userId = ctx.user?.id;
email = ctx.user?.email ?? undefined;
} catch {
// unauthenticated or auth unavailable
}
await handler(req, res);
const duration = Date.now() - start;
const meta = {
requestId,
procedure: route,
transport: "rest",
duration,
userId,
...(isCloud && email && { email }),
...(Object.keys(input).length > 0 && { input }),
status: statusCode,
};
if (statusCode < 400) {
log.info(meta, "API OK");
} else {
log.error(meta, "API error");
}
};
}

View File

@@ -1,6 +1,9 @@
import { env } from "next-runtime-env";
import type { dbClient } from "@kan/db/client";
import { createLogger } from "@kan/logger";
const log = createLogger("notifications");
import * as cardRepo from "@kan/db/repository/card.repo";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as notificationRepo from "@kan/db/repository/notification.repo";
@@ -72,6 +75,7 @@ export async function sendMentionEmails({
const baseUrl = env("NEXT_PUBLIC_BASE_URL");
const cardUrl = `${baseUrl}/cards/${cardPublicId}`;
log.info({ cardPublicId, mentionCount: membersToNotify.length, commenterUserId }, "Sending mention emails");
// Send emails to all mentioned members (only if notification doesn't exist)
await Promise.all(
membersToNotify.map(async (member) => {
@@ -91,6 +95,7 @@ export async function sendMentionEmails({
// If notification already exists, skip sending email
if (notificationExists) {
log.debug({ email, cardPublicId }, "Skipping duplicate mention email");
return;
}
@@ -114,20 +119,14 @@ export async function sendMentionEmails({
cardUrl,
},
);
log.info({ email, cardPublicId }, "Mention email sent");
} catch (error) {
console.error("Failed to send mention email:", {
email,
cardPublicId,
error: error instanceof Error ? error.message : String(error),
});
log.error({ err: error, email, cardPublicId }, "Failed to send mention email");
}
}),
);
} catch (error) {
console.error("Error sending mention emails:", {
cardPublicId,
error: error instanceof Error ? error.message : String(error),
});
log.error({ err: error, cardPublicId }, "Error sending mention emails");
}
}

View File

@@ -5,6 +5,9 @@ import {
} from "rate-limiter-flexible";
import { getRedisClient } from "@kan/db/redis";
import { createLogger } from "@kan/logger";
const log = createLogger("rateLimit");
export interface RateLimitOptions {
points?: number;
@@ -45,7 +48,7 @@ function createRateLimiter(options: RateLimitOptions = {}) {
// Use Redis if available, otherwise fall back to in-memory storage
if (redis) {
console.log("Using Redis for rate limiting");
log.debug("Using Redis for rate limiting");
return new RateLimiterRedis({
storeClient: redis,
points,
@@ -53,7 +56,7 @@ function createRateLimiter(options: RateLimitOptions = {}) {
});
}
console.log("Using in-memory for rate limiting");
log.debug("Redis unavailable, falling back to in-memory rate limiting");
return new RateLimiterMemory({
points,
duration,

View File

@@ -4,6 +4,9 @@ import { z } from "zod";
import type { dbClient } from "@kan/db/client";
import type { WebhookEvent } from "@kan/db/schema";
import * as webhookRepo from "@kan/db/repository/webhook.repo";
import { createLogger } from "@kan/logger";
const log = createLogger("webhook");
export type WebhookEventType = WebhookEvent;
@@ -199,9 +202,9 @@ export async function sendWebhooksForWorkspace(
sendWebhookToUrl(webhook.url, webhook.secret ?? undefined, payload).then(
(result) => {
if (!result.success) {
console.error(
`Webhook delivery failed to ${webhook.url}: ${result.error}`,
);
log.error({ url: webhook.url, event: payload.event, error: result.error, statusCode: result.statusCode }, "Webhook delivery failed");
} else {
log.info({ url: webhook.url, event: payload.event, statusCode: result.statusCode }, "Webhook delivered");
}
},
),
@@ -210,7 +213,7 @@ export async function sendWebhooksForWorkspace(
// Wait for all to complete but don't block on failures
await Promise.allSettled(promises);
} catch (error) {
console.error("Failed to send webhooks for workspace:", error);
log.error({ err: error, workspaceId }, "Failed to send webhooks for workspace");
}
}

View File

@@ -39,6 +39,7 @@
"prettier": "@kan/prettier-config",
"dependencies": {
"@better-auth/stripe": "^1.4.6",
"@kan/logger": "workspace:^",
"better-auth": "^1.4.6"
}
}

View File

@@ -7,8 +7,11 @@ import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import { notificationClient } from "@kan/email";
import { createLogger } from "@kan/logger";
import { createEmailUnsubscribeLink, createS3Client } from "@kan/shared";
const log = createLogger("auth");
import { downloadImage } from "./utils";
type BetterAuthUser = {
@@ -103,6 +106,7 @@ export function createDatabaseHooks(db: dbClient) {
const unsubscribeUrl = await createEmailUnsubscribeLink(user.id);
log.info({ workflowId: "user-signup", userId: user.id, email: user.email }, "Triggering Novu workflow");
await notificationClient.trigger({
to: {
subscriberId: user.id,
@@ -122,6 +126,7 @@ export function createDatabaseHooks(db: dbClient) {
},
workflowId: "user-signup",
});
log.info({ workflowId: "user-signup", userId: user.id }, "Novu workflow triggered");
await notificationClient.subscribers.credentials.update(
{
@@ -134,7 +139,7 @@ export function createDatabaseHooks(db: dbClient) {
user.id,
);
} catch (error) {
console.error("Error adding user to notification client", error);
log.error({ err: error }, "Error adding user to notification client");
}
}
},

View File

@@ -8,7 +8,10 @@ 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 { sendEmail } from "@kan/email";
import { createLogger } from "@kan/logger";
import { generateUID } from "@kan/shared/utils";
const log = createLogger("auth");
import { createStripeClient } from "@kan/stripe";
import { socialProvidersPlugin } from "./providers";
@@ -101,9 +104,7 @@ export function createPlugins(db: dbClient) {
unlimitedSeats: true,
},
);
console.log(
`Pro subscription ${stripeSubscription.id} activated with unlimited seats`,
);
log.info({ subscriptionId: stripeSubscription.id }, "Pro subscription activated with unlimited seats");
const workspace = await workspaceRepo.getByPublicId(
db,
@@ -164,6 +165,13 @@ export function createPlugins(db: dbClient) {
: []),
apiKey({
enableSessionForAPIKeys: true,
customAPIKeyGetter: (ctx) => {
const authorization = ctx.headers?.get("authorization");
if (authorization?.startsWith("Bearer ")) {
return authorization.slice(7);
}
return ctx.headers?.get("x-api-key") ?? undefined;
},
rateLimit: {
enabled: true,
timeWindow: 1000 * 60, // 1 minute
@@ -175,11 +183,7 @@ export function createPlugins(db: dbClient) {
sendMagicLink: async ({ email, url }) => {
try {
const decodedUrl = decodeURIComponent(url);
console.log("Sending magic link to:", email, "URL:", url);
console.log(
"Magic link contains invite:",
decodedUrl.includes("type=invite"),
);
log.info({ email, isInvite: decodedUrl.includes("type=invite") }, "Sending magic link");
if (decodedUrl.includes("type=invite")) {
let inviterName = "";
let workspaceName = "";
@@ -211,7 +215,7 @@ export function createPlugins(db: dbClient) {
}
}
} catch (error) {
console.error("Failed to fetch invite details:", error);
log.error({ err: error }, "Failed to fetch invite details");
}
await sendEmail(
@@ -239,11 +243,7 @@ export function createPlugins(db: dbClient) {
);
}
} catch (error) {
console.error("Error sending magic link:", {
email,
url,
error,
});
log.error({ err: error, email }, "Error sending magic link");
}
},
}),
@@ -273,7 +273,7 @@ export function createPlugins(db: dbClient) {
picture?: string;
avatar?: string;
}) => {
console.log("OIDC profile:", profile);
log.debug({ profile }, "OIDC profile received");
const name =
profile.name ??

View File

@@ -4,8 +4,11 @@ import type Stripe from "stripe";
import type { dbClient } from "@kan/db/client";
import * as userRepo from "@kan/db/repository/user.repo";
import { notificationClient } from "@kan/email";
import { createLogger } from "@kan/logger";
import { createEmailUnsubscribeLink } from "@kan/shared";
const log = createLogger("auth");
export async function downloadImage(url: string): Promise<Buffer> {
const response = await fetch(url);
if (!response.ok) {
@@ -32,6 +35,7 @@ export async function triggerWorkflow(
const unsubscribeUrl = await createEmailUnsubscribeLink(user.id);
log.info({ workflowId, userId: user.id }, "Triggering Novu workflow");
await notificationClient.trigger({
to: {
subscriberId: user.id,
@@ -43,7 +47,8 @@ export async function triggerWorkflow(
},
workflowId,
});
log.info({ workflowId, userId: user.id }, "Novu workflow triggered");
} catch (error) {
console.error("Error triggering workflow", error);
log.error({ err: error, workflowId }, "Error triggering workflow");
}
}

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."workspace_plan" ADD VALUE 'team' BEFORE 'pro';

File diff suppressed because it is too large Load Diff

View File

@@ -218,6 +218,13 @@
"when": 1773212242728,
"tag": "20260311065722_AddWeekStartDay",
"breakpoints": true
},
{
"idx": 31,
"version": "7",
"when": 1775170588247,
"tag": "20260402225628_AddTeamWorkspacePlan",
"breakpoints": true
}
]
}

View File

@@ -44,6 +44,7 @@
},
"dependencies": {
"@electric-sql/pglite": "^0.3.7",
"@kan/logger": "workspace:^",
"@kan/shared": "workspace:^",
"drizzle-orm": "^0.42.0",
"drizzle-zod": "^0.5.1",

View File

@@ -6,8 +6,12 @@ import { drizzle as drizzlePgLite } from "drizzle-orm/pglite";
import { migrate } from "drizzle-orm/pglite/migrator";
import { Pool } from "pg";
import { createLogger } from "@kan/logger";
import * as schema from "./schema";
const log = createLogger("db");
export type dbClient = NodePgDatabase<typeof schema> & {
$client: Pool;
};
@@ -16,7 +20,7 @@ export const createDrizzleClient = (): dbClient => {
const connectionString = process.env.POSTGRES_URL;
if (!connectionString) {
console.log("POSTGRES_URL environment variable is not set, using PGLite");
log.warn("POSTGRES_URL not set, falling back to PGLite");
const client = new PGlite({
dataDir: "./pgdata",

View File

@@ -402,8 +402,6 @@ export const softDeleteById = async (
.groupBy(lists.index)
.having(gt(countExpr, 1));
console.log(duplicateIndices);
if (duplicateIndices.length > 0) {
throw new Error(
`Duplicate indices found after reordering in board ${result.boardId}`,

View File

@@ -64,6 +64,8 @@ export const create = async (
slug: string;
createdBy: string;
createdByEmail: string;
description?: string;
plan?: "free" | "team" | "pro" | "enterprise";
},
) => {
const [workspace] = await db
@@ -73,6 +75,8 @@ export const create = async (
name: workspaceInput.name,
slug: workspaceInput.slug,
createdBy: workspaceInput.createdBy,
...(workspaceInput.description && { description: workspaceInput.description }),
...(workspaceInput.plan && { plan: workspaceInput.plan }),
})
.returning({
id: workspaces.id,
@@ -124,7 +128,7 @@ export const update = async (
workspaceInput: {
name?: string;
slug?: string;
plan?: "free" | "pro" | "enterprise";
plan?: "free" | "team" | "pro" | "enterprise";
description?: string;
showEmailsToMembers?: boolean;
weekStartDay?: number;

View File

@@ -34,7 +34,7 @@ export const slugTypes = ["reserved", "premium"] as const;
export type SlugType = (typeof slugTypes)[number];
export const slugTypeEnum = pgEnum("slug_type", slugTypes);
export const workspacePlans = ["free", "pro", "enterprise"] as const;
export const workspacePlans = ["free", "team", "pro", "enterprise"] as const;
export type WorkspacePlan = (typeof workspacePlans)[number];
export const workspacePlanEnum = pgEnum("workspace_plan", workspacePlans);

View File

@@ -23,6 +23,7 @@
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@kan/logger": "workspace:^",
"@novu/api": "^3.11.0",
"@react-email/components": "^1.0.1",
"nodemailer": "^7.0.3",

View File

@@ -1,5 +1,8 @@
import { render } from "@react-email/render";
import nodemailer from "nodemailer";
import { createLogger } from "@kan/logger";
const log = createLogger("email");
import JoinWorkspaceTemplate from "./templates/join-workspace";
import MagicLinkTemplate from "./templates/magic-link";
@@ -44,6 +47,7 @@ export const sendEmail = async (
template: Templates,
data: Record<string, string>,
) => {
log.info({ to, subject, template }, "Sending email");
try {
const EmailTemplate = emailTemplates[template];
@@ -62,16 +66,10 @@ export const sendEmail = async (
throw new Error(`Failed to send email: ${response.response}`);
}
log.info({ to, subject, template, messageId: response.messageId }, "Email sent");
return response;
} catch (error) {
console.error("Email sending failed:", {
to,
from: process.env.EMAIL_FROM,
subject,
template,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
log.error({ err: error, to, from: process.env.EMAIL_FROM, subject, template }, "Email sending failed");
throw error;
}
};

View File

@@ -0,0 +1,35 @@
{
"name": "@kan/logger",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./src/index.ts"
}
},
"license": "GPL-3.0",
"scripts": {
"build": "tsc",
"clean": "git clean -xdf .cache .turbo dist node_modules",
"dev": "tsc",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@axiomhq/js": "^1.4.0",
"pino": "^9.14.0",
"pino-pretty": "^13.1.3"
},
"devDependencies": {
"@kan/eslint-config": "workspace:*",
"@kan/prettier-config": "workspace:*",
"@kan/tsconfig": "workspace:*",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
},
"prettier": "@kan/prettier-config"
}

View File

@@ -0,0 +1,43 @@
import { Axiom } from "@axiomhq/js";
import pino from "pino";
const isDev = process.env.NODE_ENV !== "production";
const isCloud = process.env.NEXT_PUBLIC_KAN_ENV === "cloud";
const level = process.env.LOG_LEVEL || (isDev ? "debug" : "info");
const axiomToken = process.env.AXIOM_TOKEN;
const axiomDataset = process.env.AXIOM_DATASET;
const useAxiom = isCloud && !!axiomToken && !!axiomDataset;
function createAxiomStream(token: string, dataset: string): pino.DestinationStream {
const client = new Axiom({ token });
return {
write(msg: string) {
try {
client.ingest(dataset, [JSON.parse(msg) as Record<string, unknown>]);
} catch {
// ignore malformed log lines
}
},
};
}
export const logger = useAxiom
? pino(
{ level },
pino.multistream([
{ stream: process.stdout, level },
{ stream: createAxiomStream(axiomToken, axiomDataset), level },
]),
)
: pino({
level,
...(isDev && {
transport: {
target: "pino-pretty",
options: { colorize: true, ignore: "pid,hostname", translateTime: "HH:MM:ss" },
},
}),
});
export const createLogger = (module: string) => logger.child({ module });

View File

@@ -0,0 +1,6 @@
{
"extends": "@kan/tsconfig/internal-package.json",
"compilerOptions": {},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

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

Some files were not shown because too many files have changed in this diff Show More