Compare commits

..

15 Commits

Author SHA1 Message Date
Morfixx
2a0220ce6f feat: added docs to website (#186)
* feat(docs): self-hosting s3 minio guide

* feat(docs): self-hosting introduction guide

* fix: build error

* refactor: use kan over kan.bn

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
2025-09-19 21:08:59 +01:00
Morfixx
7ccd2ac1e0 feat(web): added image cropping to avatar uploads (#185)
* feat(web): added image cropping to avatar uploads

* chore: add translations

* refactor: move react crop css import to avatar component

* refactor: remove duplicate interface

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
2025-09-19 20:44:20 +01:00
Henry
cea5cf84c8 feat(cloud): pause/reactivate members (#184)
* feat: add paused member status

* feat: add paused badge to member table

* chore: add translations

* feat: unpause all members on pro activation
2025-09-18 22:09:42 +01:00
Henry
6738fddc5f feat(localisation): add russian language support (#183) 2025-09-18 20:50:01 +01:00
Henry
dc1f78df55 fix: set correct grid size for boards on desktop viewports (#180) 2025-09-14 21:12:35 +01:00
Henry
87e02fdcb0 chore: add missing translations 2025-09-14 20:46:11 +01:00
Henry
7073cd5931 feat: only render table if api keys exist 2025-09-14 20:28:38 +01:00
LovelessCodes
3e21b23f0a refactor: reorganize settings page with tabbed interface (#57)
* refactor: reorganize settings page with tabbed interface

* feat: revamp API key management with new list view and confirmation modals

* refactor: update tab styling

* refactor: tweak UI/UX for managing API keys

* refactor: only show update button when change has been made

* refactor: only show update button when content of display name has been updated

* feat: store tab state in params

* refactor: remove focus state from tabs

* refactor: tweak styling on mobile select

* refactor: simplify settings pages

* feat: open upgrade modal if upgrade=pro is in params

* feat: add scroll to api key list on mobile

* chore: add translations

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
2025-09-14 15:48:55 +01:00
Henry
793baa8325 fix: update modal z-index to ensure proper layering (#177) 2025-09-12 20:14:41 +01:00
Henry
63e639e337 fix: show correct active workspace and truncate name (#175) 2025-09-11 23:02:48 +01:00
Henry
c2ccb6b13b fix: prevent card side menu selectors from closing unexpectedly (#174) 2025-09-11 22:48:53 +01:00
Henry
4ee1d6f2d5 fix: prevent new workspace modal from getting stuck in an open state (#173) 2025-09-11 22:29:48 +01:00
Henry
2f88366418 feat: set workspace url when creating a new workspace (#172)
* feat: add migration to remove not null constraint from referenceId on subscriptions

* feat(cloud): write worspace slug availability checks to db

* feat: add migration to add cascade set null to referenceId on subscription

* feat: update workspace and subscripton schema

* feat: add createWorkspaceSlugCheck repo func

* feat: set workspace url on creation

* chore: update translations

* refactor: various tweaks and fixes

* feat: add pro bolt to pricing notice

* feat: toggle pro subscription

* fix: hide pro subscription notice for self hosters

* feat: add pro upgrade message on members page

* feat: hide pro upgrade notice on small viewports

* feat: update workspace url placeholder
2025-09-11 20:32:02 +01:00
Henry
4f7fa1a228 fix: compile translations 2025-09-09 13:42:43 +01:00
Henry
308cb22729 feat(cloud): pro launch features (#170)
* fix: ensure workspace slug is updated on activation

* feat: open upgrade modal if upgrade=pro is in URL params

* fix: show correct skeleton loader for workspace menu when collapsed

* fix: reenable scroll when navigating back to marketing page

* feat: add launch offer notice
2025-09-09 13:07:39 +01:00
82 changed files with 16678 additions and 1234 deletions

View File

@@ -0,0 +1,140 @@
---
title: "Introduction"
description: "Overview and quick start to run Kan on your own infrastructure using Docker Compose."
mode: "wide"
tag: "NEW"
---
This guide introduces how to self-host Kan. It starts with the minimal Docker Compose setup (web + PostgreSQL) and points you to optional features like email and S3-based file storage.
## What youll set up
<CardGroup cols={2}>
<Card title="Kan" icon="globe" color="#0284c7" horizontal>
Next.js application served on port 3000.
</Card>
<Card title="PostgreSQL 15" icon="database" color="#65a30d" horizontal>
Primary database for Kan data.
</Card>
</CardGroup>
<Note>
For file uploads (avatars), OAuth, and other advanced options, see the
Environment Variables section in the README and the dedicated [S3
guide](/guides/self-hosting/s3). The [full
compose](https://github.com/kanbn/kan/blob/main/docker-compose.yml) in the
repo includes a richer configuration via <code>.env</code>.
</Note>
## Prerequisites
- Docker and Docker Compose
- A long random string for <code>BETTER_AUTH_SECRET</code> (32+ chars)
## Quick start
<Steps>
<Step title="Create a docker-compose.yml">
Paste the following minimal configuration into a new <code>docker-compose.yml</code> file:
```yaml
services:
web:
image: ghcr.io/kanbn/kan:latest
container_name: kan-web
ports:
- "3000:3000"
networks:
- kan-network
environment:
NEXT_PUBLIC_BASE_URL: http://localhost:3000
BETTER_AUTH_SECRET: your_auth_secret
POSTGRES_URL: postgresql://kan:your_postgres_password@postgres:5432/kan_db
NEXT_PUBLIC_ALLOW_CREDENTIALS: true
depends_on:
- postgres
restart: unless-stopped
postgres:
image: postgres:15
container_name: kan-db
environment:
POSTGRES_DB: kan_db
POSTGRES_USER: kan
POSTGRES_PASSWORD: your_postgres_password
ports:
- 5432:5432
volumes:
- kan_postgres_data:/var/lib/postgresql/data
restart: unless-stopped
networks:
- kan-network
networks:
kan-network:
volumes:
kan_postgres_data:
```
<Tip>
The example above is intentionally minimal. The repository provides a more feature-complete compose file at [docker-compose.yml](https://github.com/kanbn/kan/blob/main/docker-compose.yml) if you want environment-based configuration, OAuth, S3, and more.
</Tip>
</Step>
<Step title="Start the stack">
Bring everything up in detached mode:
```bash
docker compose up -d
```
Once started, open [http://localhost:3000](http://localhost:3000).
</Step>
<Step title="Manage the containers">
Useful commands while developing or testing:
- Stop the containers: <code>docker compose down</code>
- View logs: <code>docker compose logs -f</code>
- Restart: <code>docker compose restart</code>
</Step>
<Step title="Configure environment (optional)">
For a production-like setup and more features (email, OAuth, file uploads, etc.), create a <code>.env</code> file and set the relevant variables shown in the READMEs Environment Variables section.
<Accordion title="Common variables">
```bash
# Required
NEXT_PUBLIC_BASE_URL=http://localhost:3000
BETTER_AUTH_SECRET=replace_with_long_random_string
POSTGRES_URL=postgresql://kan:your_postgres_password@postgres:5432/kan_db
# Optional: Email
EMAIL_FROM="Kan <hello@mail.kan.bn>"
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_USER=resend
SMTP_PASSWORD=re_xxxx
SMTP_SECURE=true
# Optional: Auth toggles
NEXT_PUBLIC_ALLOW_CREDENTIALS=true
NEXT_PUBLIC_DISABLE_SIGN_UP=false
```
<Note type="warning">
If you plan to enable file uploads (avatars, etc.), youll also need S3 variables (<code>S3_ENDPOINT</code>, <code>S3_ACCESS_KEY_ID</code>, <code>S3_SECRET_ACCESS_KEY</code>, <code>NEXT_PUBLIC_STORAGE_URL</code>, <code>NEXT_PUBLIC_STORAGE_DOMAIN</code>, …). See the S3 guide linked at the top.
</Note>
</Accordion>
</Step>
</Steps>
## Reference
- [GitHub README](https://github.com/kanbn/kan/blob/main/README.md#self-hosting-)
- [GitHub docker-compose.yml](https://github.com/kanbn/kan/blob/main/docker-compose.yml)

View File

@@ -0,0 +1,397 @@
---
title: "Kan + MinIO (S3)"
mode: "wide"
tag: "NEW"
---
Deploy Kan with PostgreSQL and MinIO (S3-compatible storage) using Docker Compose, with clear steps and production notes.
## What youll set up
<CardGroup cols={2}>
<Card title="Kan" icon="globe" color="#0284c7" horizontal>
Kan web app (Next.js), on port 3000.
</Card>
<Card title="PostgreSQL 15" icon="database" color="#65a30d" horizontal>
PostgreSQL database for Kan.
</Card>
</CardGroup>
<Card title="MinIO (S3-compatible)" icon="cloud" color="#ca8a04" horizontal>
MinIO object storage (console port 9001, S3 API port 9000).
</Card>
## How it works
- Kan stores data in PostgreSQL.
- Kan uploads files (e.g., avatars) to MinIO over the S3 API.
- The browser fetches public files directly from MinIOs public URL.
- The Next.js image optimizer in Kan must be explicitly allowed to fetch from your storage host.
Key domain settings:
- <code>NEXT_PUBLIC_BASE_URL</code> → the Kan site
- <code>NEXT_PUBLIC_STORAGE_URL</code> → the public S3 base URL
- <code>NEXT_PUBLIC_STORAGE_DOMAIN</code> → the exact S3 hostname (no scheme)
## Prerequisites
- Docker and Docker Compose
- Open local ports: 3000 (Kan), 5432 (Postgres), 9000/9001 (MinIO)
- A long random string for <code>BETTER_AUTH_SECRET</code> (32+ chars)
<Note type="warning">
For production youll want a reverse proxy (Traefik/Nginx/Caddy), valid TLS
certificates, and DNS for your domains (e.g., <code>kan.example.com</code>,{" "}
<code>s3.example.com</code>).
</Note>
## Quick start
<Tip type="info">
Why <code>localtest.me</code>? It resolves to <code>127.0.0.1</code>{" "}
automatically, so you can test domain-based configs locally without editing
hosts.
</Tip>
<Steps>
<Step title="Set environment variables">
Provide the minimum required configuration (local example):
```bash
NEXT_PUBLIC_BASE_URL=http://kan.localtest.me:3000
BETTER_AUTH_SECRET=<long random string>
POSTGRES_URL=postgresql://kan:<password>@postgres:5432/kan_db
# MinIO/S3
S3_ENDPOINT=http://s3.localtest.me:9000
S3_ACCESS_KEY_ID=<minio-access-key>
S3_SECRET_ACCESS_KEY=<minio-secret-key>
S3_REGION=none
S3_FORCE_PATH_STYLE=true
# Public storage access
NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000
NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me
NEXT_PUBLIC_AVATAR_BUCKET_NAME=kan
```
<Note type="info">
Issue #109 fix: make sure <code>NEXT_PUBLIC_STORAGE_DOMAIN</code> exactly
equals the hostname that serves your images (no scheme, no port).
</Note>
Optional (see README for full list): Email (`EMAIL_FROM`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_SECURE`), OAuth/OIDC (`GOOGLE_*`, `GITHUB_*`, `OIDC_*`), auth toggles, Trello import, etc.
</Step>
<Step title="Create or review Docker Compose files">
You can start from the minimal compose at the repository root (<code>docker-compose.yml</code>) and review the production-oriented settings in <code>cloud/docker-compose.yml</code>.
Start with the minimal setup (web + postgres + minio) and ensure environment variables are passed to the web service.
<Accordion title="Docker Compose example">
```yaml
services:
postgres:
image: postgres:15
container_name: kan-db
ports:
- "5432:5432"
environment:
POSTGRES_USER: kan
POSTGRES_PASSWORD: changeme
POSTGRES_DB: kan_db
volumes:
- pg_data:/var/lib/postgresql/data
restart: unless-stopped
minio:
image: minio/minio:latest
container_name: kan-minio
command: server /data --console-address ":9001"
ports:
- "9000:9000" # S3 API
- "9001:9001" # Console
environment:
# Use the same credentials in your .env
# as S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio123456789
volumes:
- minio_data:/data
restart: unless-stopped
web:
image: ghcr.io/kanbn/kan:latest
container_name: kan-web
depends_on:
- postgres
- minio
ports:
- "3000:3000"
# Load variables from .env
# (see the "Set environment variables" step)
env_file:
- .env
restart: unless-stopped
volumes:
pg_data:
minio_data:
```
<Note type="info">
Ensure your <code>.env</code> contains values that match this compose file.
For example:
<ul>
<li>
<code>POSTGRES_URL=postgresql://kan:changeme@postgres:5432/kan_db</code>
</li>
<li>
<code>S3_ENDPOINT=http://s3.localtest.me:9000</code>
</li>
<li>
<code>S3_ACCESS_KEY_ID=minio</code> and{" "}
<code>S3_SECRET_ACCESS_KEY=minio123456789</code>
</li>
<li>
<code>S3_FORCE_PATH_STYLE=true</code>
</li>
<li>
<code>NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000</code>
</li>
<li>
<code>NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me</code>
</li>
<li>
<code>NEXT_PUBLIC_AVATAR_BUCKET_NAME=kan</code>
</li>
</ul>
</Note>
</Accordion>
</Step>
<Step title="Start services">
```bash
docker compose up -d
```
Then open:
<ul>
<li>
Kan: <a href="http://kan.localtest.me:3000">http://kan.localtest.me:3000</a>
</li>
<li>
MinIO Console:{" "}
<a href="http://minio.localtest.me:9001">http://minio.localtest.me:9001</a>
</li>
<li>
MinIO S3 API:{" "}
<a href="http://s3.localtest.me:9000">http://s3.localtest.me:9000</a>
</li>
</ul>
</Step>
<Step title="Initialize MinIO">
1) Log into the MinIO Console (http://minio.localtest.me:9001).
2. Create a bucket (e.g., <code>kan</code>).
3. For simple public avatars, apply a read-only policy so GET requests are allowed for objects:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": ["*"] },
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::kan"]
},
{
"Effect": "Allow",
"Principal": { "AWS": ["*"] },
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::kan/*"]
}
]
}
```
<Note type="warning">
Alternatively, keep the bucket private and use presigned URLs. In that case,
ensure your server and browser access paths are correctly configured.
</Note>
</Step>
<Step title="Verify the setup">
<ul>
<li>Sign in to Kan and upload an avatar (Settings).</li>
<li>Confirm the object is created in your MinIO bucket.</li>
<li>The avatar should render without errors.</li>
</ul>
If you see a 400 from <code>/\_next/image</code> with “url parameter is not allowed”:
<ul>
<li>
<code>NEXT_PUBLIC_STORAGE_DOMAIN</code> must exactly match the S3 hostname
that serves images.
</li>
<li>
<code>NEXT_PUBLIC_STORAGE_URL</code> should use the same host (with
scheme/port).
</li>
<li>Ensure youre using the latest Kan image.</li>
</ul>
</Step>
</Steps>
## Production setup
<Tabs>
<Tab title="Local">
<ul>
<li><code>NEXT_PUBLIC_BASE_URL=http://kan.localtest.me:3000</code></li>
<li><code>S3_ENDPOINT=http://s3.localtest.me:9000</code></li>
<li><code>NEXT_PUBLIC_STORAGE_URL=http://s3.localtest.me:9000</code></li>
<li><code>NEXT_PUBLIC_STORAGE_DOMAIN=s3.localtest.me</code></li>
<li>Keep <code>S3_FORCE_PATH_STYLE=true</code> for MinIO.</li>
</ul>
</Tab>
<Tab title="Production">
<ul>
<li><code>NEXT_PUBLIC_BASE_URL=https://kan.example.com</code></li>
<li><code>S3_ENDPOINT=https://s3.example.com</code></li>
<li><code>NEXT_PUBLIC_STORAGE_URL=https://s3.example.com</code></li>
<li><code>NEXT_PUBLIC_STORAGE_DOMAIN=s3.example.com</code></li>
<li>Keep <code>S3_FORCE_PATH_STYLE=true</code> for MinIO.</li>
<li>Put Kan and MinIO behind HTTPS with a reverse proxy (Traefik/Nginx) and valid TLS.</li>
</ul>
</Tab>
</Tabs>
## Troubleshooting
<AccordionGroup>
<Accordion title="Files upload but dont display">
<ul>
<li>If public: confirm GET is allowed on objects (bucket policy).</li>
<li>If private: ensure presigned URLs are generated and valid.</li>
<li>403 AccessDenied indicates permissions, not CORS. CORS is not required for simple <code>&lt;img&gt;</code> GETs.</li>
</ul>
</Accordion>
<Accordion title="Make the bucket public (read-only) with mc" defaultOpen>
Use your MinIO root credentials to allow anonymous reads:
```bash
# Replace with your MINIO_ROOT_PASSWORD
MINIO_PASS='<your-minio-password>'
# Point mc at MinIO via the container network (no ports required)
docker run --rm --network container:kan-minio minio/mc \
mc alias set local http://127.0.0.1:9000 minio "$MINIO_PASS"
# Allow public downloads from the bucket
docker run --rm --network container:kan-minio minio/mc \
mc anonymous set download local/kan
# Optional: verify anonymous status
docker run --rm --network container:kan-minio minio/mc \
mc anonymous get local/kan
```
</Accordion>
<Accordion title="Alternative: S3 bucket policy (AWS CLI)">
If you prefer a bucket policy, apply a public-read policy for objects:
```json policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::kan/*"]
}
]
}
```
```bash
# Use your MinIO root credentials
MINIO_PASS='<your-minio-password>'
docker run --rm --network container:kan-minio \
-e AWS_ACCESS_KEY_ID=minio \
-e AWS_SECRET_ACCESS_KEY="$MINIO_PASS" \
-e AWS_DEFAULT_REGION=us-east-1 -e AWS_S3_FORCE_PATH_STYLE=true \
-v "$PWD:/work" amazon/aws-cli \
s3api put-bucket-policy --bucket kan --policy file:///work/policy.json \
--endpoint-url http://127.0.0.1:9000
```
</Accordion>
<Accordion title="Next.js optimizer 400 — url parameter is not allowed">
<ul>
<li>
Exact match on <code>NEXT_PUBLIC_STORAGE_DOMAIN</code> with your storage
host.
</li>
<li>
Same host in <code>NEXT_PUBLIC_STORAGE_URL</code>.
</li>
<li>Update to the latest Kan image.</li>
</ul>
</Accordion>
<Accordion title="Next/Image: “url parameter is valid but upstream response is invalid”">
<ul>
<li>This means Next.js accepted the URL, but the upstream returned a non-image (e.g., 403 HTML/XML).</li>
<li>Fix: make the bucket/object publicly readable (see above), or use presigned URLs.</li>
<li>Sanity test from the web network (replace with your image URL):</li>
</ul>
```bash
IMG_URL="https://s3.example.com/kan/path/to/avatar.jpg"
# Headers/content-type as seen from the app network
docker run --rm --network container:kan-web curlimages/curl:8.9.1 \
-I -L --max-redirs 5 "$IMG_URL"
# Quick status + content-type summary
docker run --rm --network container:kan-web curlimages/curl:8.9.1 \
-s -o /dev/null -w "HTTP:%{http_code} CT:%{content_type} URL:%{url_effective}\n" -L "$IMG_URL"
```
</Accordion>
<Accordion title="Connectivity checks">
<ul>
<li>The Kan container must reach <code>S3_ENDPOINT</code>.</li>
<li>Verify DNS/ports inside the container (e.g., <code>docker exec -it &lt;kan-container&gt; sh</code>).</li>
</ul>
</Accordion>
</AccordionGroup>
## References
- [Kan README](https://github.com/kanbn/kan/blob/main/README.md)
- [Cloud compose reference](https://github.com/kanbn/kan/blob/main/cloud/docker-compose.yml)
- [Kan #109 Issue](https://github.com/kanbn/kan/issues/109)

View File

@@ -44,6 +44,18 @@
"group": "Get Started",
"pages": ["introduction"]
},
{
"group": "Guides",
"pages": [
{
"group": "Self-Hosting",
"pages": [
"guides/self-hosting/introduction",
"guides/self-hosting/s3"
]
}
]
},
{
"group": "Import",
"pages": ["imports/trello"]

View File

@@ -2,7 +2,7 @@
"version": 0,
"locale": {
"source": "en",
"targets": ["fr", "de", "es", "it", "nl"]
"targets": ["fr", "de", "es", "it", "nl", "ru"]
},
"buckets": {
"po": {

View File

@@ -10,6 +10,7 @@ checksums:
"%248%2Fmonth/singular": 4667034934bb2bc3d569c70b94205b51
1%20user/singular: 3b547431ab12f0fba84307e6a81109d8
A%20powerful%2C%20flexible%20kanban%20app%20that%20helps%20you%20organise%20work%2C%20track%20progress%2C%20and%20deliver%20results%E2%80%94all%20in%20one%20place./singular: d405b83b0d631cb72f4347c10bcbb643
Account/singular: 01215c12fb1cdb93bd0c84c1382bef56
Account%20deleted/singular: a25da96a1579c4491be0a95669ef18a4
Activity/singular: 1948763de8e531483a798b68195e297e
Activity%20logs/singular: 8b1f0bb96a905646ecfad1cfdfa42168
@@ -29,11 +30,17 @@ checksums:
added%20checklist%20item%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: bdd202da20b1fffbec21792c5453f90c
added%20label%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: b32be052b3d57de0c9120fa7f9fc86ee
Adding%20a%20new%20member%20will%20cost%20an%20additional%20%7Bprice%7D%20(%7BbillingType%7D)%20per%20seat./singular: 12e88573028306110fbc15ef1e714892
Adjust%20the%20square%20crop%20to%20fit%20your%20avatar./singular: a4df26bbce6f14c6962fac1324db00a8
Admin%20roles/singular: 32a5d78073b9bb9a246773afba8831df
All%20systems%20operational/singular: ee943a4046b09e6334cceeea9fda2bfc
Already%20have%20an%20account%3F%20%3C0%3E%3C1%3ESign%20in%3C%2F1%3E%3C%2F0%3E/singular: 2959fd276248208b65cb27ed46b20135
An%20error%20occurred%20while%20disconnecting%20your%20Trello%20account./singular: 0aa3973b860c1faf8d9123aebf567e40
An%20unexpected%20error%20occurred.%20Please%20try%20again%20later./singular: 1b5749b0cca6a62d75a577bee6804074
API/singular: 01d9819514e27056dcc69463194b63d2
API%20key%20created/singular: 8dbb2b60a719b0d120e774d6666c8c45
API%20key%20name/singular: 2d8aeb08b2cce3b750a584bbc5ce6d1d
API%20key%20name%20cannot%20exceed%2030%20characters/singular: 3ea1c7d68e1074a75128017b097b727a
API%20key%20name%20is%20required/singular: 870970743b14cb56e7fb7b410af0e933
API%20keys/singular: 07f3620a30e08136f0b072c9af8d1eef
API%20Reference/singular: 7dcc877064bfdf889ec55dbc9e06a242
Applicants/singular: 4babd8331a1441e91087f855f43e57f8
@@ -55,7 +62,7 @@ checksums:
Billing%20portal/singular: dc3afa14ffe19f5920992a0c9d49d525
Blog%20Post/singular: 38e027f212be445dc530e427fc642a20
Board/singular: 101ff39aab674c033c15ab978d0d8bac
Board%20analytics%20(coming%20soon)/singular: 3f648e6fe3849ee2ac5cdf410ac9ea06
Board%20analytics/singular: 336a8c70a91adc49c15266167ee84cc0
Board%20name%20cannot%20exceed%20100%20characters/singular: 985c0177744ec56d8dac84f66ec284ca
Board%20name%20is%20required/singular: dbd0c6cc945ab19e702fc27f970a023b
Board%20not%20found/singular: 5af9844595fa8aed5985066620435515
@@ -74,11 +81,12 @@ checksums:
Card%20not%20found/singular: 91509e2f92b0b3b11330b6983139fdbf
Card%20title/singular: 7c34f59f4005e6cb3a6ff546ea0b96e3
Change%20Password/singular: a552fc5c4189ebc3e2e6018edda7d18f
Change%20the%20language%20of%20the%20app./singular: fb20db145e28ed44aba89c146ded7bfc
Change%20your%20language%20preferences./singular: 293d49fc3c75e9c425b64bd7126e6b46
Check%20your%20inbox/singular: e9a430fcd298def74212238df0f680d6
Checklist%20name/singular: 5eb5de823f7ca5a4d97bb41e6a3f675a
Clear%20filters/singular: 8f40ab5af527e4b190da94e7b6221379
Click%20on%20the%20link%20we've%20sent%20to%20%7BmagicLinkRecipient%7D%20to%20sign%20in./singular: 210b6ff8727f976182ec3f29ea3c7667
Close/singular: 2c2e22f8424a1031de89063bd0022e16
Code%20Review/singular: a2da6c2339301e7c3ddf068c4ff9f7e8
Collaborate%20seamlessly%20with%20your%20team./singular: c5f10e431aaf8b51519bc009a4a4080c
Coming%20soon/singular: ee2b0671e00972773210c5be5a9ccb89
@@ -99,6 +107,7 @@ checksums:
Continue%20with%20%7B0%7D/singular: 2eaf6e1da91e208f7c5fb6bf862fe8a6
Control%20who%20can%20view%20and%20edit%20your%20boards./singular: 2a7e0bec29bac26280de707e2fe8bce5
Create%20another/singular: 2de8a82a416eb78c0462aa36278edc9a
Create%20API%20key/singular: 70ed8431c6ed5f7fdef122cc34f75b41
Create%20board/singular: 155b62818bfab0e34f0089e1b34a32f3
Create%20card/singular: 32792935dd5837a9433909b04021c22b
Create%20checklist/singular: 5cbca15a7004558c4e6d381f83a34da2
@@ -111,8 +120,10 @@ checksums:
Create%20workspace/singular: 2e6718e79964ea5ce22d76c2189c77ca
created%20the%20card/singular: 605475f5aaeb4dbccbf7c4eb9107b43d
Critical/singular: eb327cd411b50aee954f8d1d215d003a
Crop%20your%20avatar/singular: eb25e2d5972ec36a0b40481c8136ab15
Current%20password%20is%20required/singular: 72536bca9598680027f2be8ce80ac280
Custom%20domain/singular: b09e7a9c187b7163b4a6cfc78042fe42
Custom%20URLs%20require%20upgrading%20to%20a%20Pro%20plan/singular: f7275e3b473b8f7b39dab6b37eb26fea
Custom%20workspace%20URL/singular: 7ba841d0946eb04fa3d17d74365be37b
Customer%20Support/singular: 50e3c77e22e41061ca85ea2f02625a2e
Dark/singular: 73e6e208ba628b26e90fcf6dce15e1b2
@@ -166,6 +177,7 @@ checksums:
Error%20updating%20workspace%20name/singular: 46025f42a7d5eff689e9cb8bed14a1e4
Error%20updating%20workspace%20URL/singular: 9da6c012e1b512d8c7fbd812626c4675
Error%20upgrading%20subscription/singular: 57cc47581c7faaffb3b1721b6352b576
Error%20upgrading%20to%20Pro/singular: 934c6867d9bdbe6099b2c0f5a3107f78
Error%20uploading%20profile%20image/singular: 0ba02753a797a74c6ea7b622c9f69c71
Everything%20in%20the%20free%20plan%2C%20plus%3A/singular: 62b44c4973b92b806c69a4b15e0256dc
Everything%20you%20need%2C%20free%20forever.%20Unlimited%20boards%2C%20unlimited%20lists%2C%20unlimited%20cards.%20Upgrade%20any%20time./singular: fa21632ab1468edf10acda2fe7b71323
@@ -231,6 +243,7 @@ checksums:
Labels%20%26%20Filters/singular: 8f0bdd6084516f8241cb613178114390
Language/singular: 277fd1a41cc237a437cd1d5e4a80463b
Launch%20offer/singular: 624592ba2ecf647a3fe60d493c1ee71a
Launch%20offer%3A%20Get%20unlimited%20members%20with%20Pro/singular: 8c1be80f5b2fcc112922282afbe30287
Launch%20offer%3A%20unlimited%20seats%20for%20just%20%2429%2Fmonth%20with%20Pro/singular: 235d1cda21811c296afdc0dcc438cc24
Learning/singular: 90e91da3f870f065f2da9aa9adf53514
Legal/singular: 00d3f08da61a21887009819cf93fb414
@@ -256,6 +269,7 @@ checksums:
Name/singular: 9368b5a047572b6051f334af5aa76819
Need%20help%3F/singular: 04e7322f2d3ffb2d73ff2f64b71637c8
New/singular: 126d036fae5fb6b629728ecb97e6195b
New%20API%20key/singular: db3088aedba6e4a99b46451c5b3d36ed
New%20board/singular: 63f4e979e29a7fc2f5c09ff91fa75966
New%20card/singular: a33f6219a756127f91c2523cfe845a19
New%20checklist/singular: 58252b71e9693ae0f4d2d0b72108a569
@@ -283,6 +297,7 @@ checksums:
Password%20Changed/singular: 1fcebe9ddb46f722a57f195efddc695d
Password%20must%20be%20at%20least%208%20characters/singular: 4c30501d085eaccea47af34212bb26a7
Passwords%20do%20not%20match/singular: 37ca1f4e0afc9a0b8e9617f767103c92
Paused/singular: edb1f7b7219e1c9b7aa67159090d6991
Payment%20frequency/singular: 63ded0e4ffb462ca8bd33d38e4691d86
Pending/singular: 030a6f3395d5d4efddd3cc67d6009039
per%20user%2Fmonth/singular: 72af182c1ba6df6732640f4d8a78d360
@@ -321,7 +336,6 @@ checksums:
Resolution/singular: 6d8bd9e1bd7dae5ae38c93061d32990e
Resources/singular: ec7fb05ed963bb6781a35782b3475502
Review/singular: 299f75db25382980b2895622d7712927
Revoke/singular: be57685a85b6dfeaeb6eab4e9560b520
Roadmap/singular: c60f4a1acf30e566861bf130f13b9ae7
Role/singular: 53743bbb6ca938f5b893552e839d067f
Run%20on%20your%20own%20infrastructure/singular: eba804911562b8dbf9d69c3e27f1d708
@@ -336,7 +350,11 @@ checksums:
Send%20feedback/singular: 9631cc08d49da04475b30a0d320ce97c
Senior/singular: 3fff865dc00435f82896fc302ea45630
Settings/singular: 8df6777277469c1fd88cc18dde2f1cc3
Settings%20%7C%20%7B0%7D/singular: b8fc73080bc9c8f4f1403b2a69bd1ac5
Settings%20%7C%20Account/singular: 050e18406849ec057edac877c297c3e1
Settings%20%7C%20API/singular: 85101e4b802a09ad9e3f01ff116f0894
Settings%20%7C%20Billing/singular: e44cba741d5414035a0b499c5766c203
Settings%20%7C%20Integrations/singular: d04992e28016452f6d3d7dcc0b592415
Settings%20%7C%20Workspace/singular: 5d0bacf7ff696da940f232df45edfd39
Sign%20in/singular: cb8757c7450e17de1e226e82fb0fa4a2
Sign%20up%20%7C%20kan.bn/singular: f3de2a110c90358e6eac07d0b2f663a6
Sign%20up%20disabled/singular: 9581b1f75b404ac0ecb7e603e0d4189c
@@ -364,14 +382,18 @@ checksums:
Theme/singular: 21fe00b7a518089576fb83c08631107a
They%20won't%20be%20able%20to%20access%20this%20workspace./singular: 93b740350fe3430319fbca85349e41d9
This%20action%20can't%20be%20undone./singular: cb222ff89715d8c971e8c25d121e1dbd
This%20API%20key%20will%20only%20be%20shown%20once.%20Please%20save%20it%20in%20a%20secure%20location./singular: 7df18d2978d317375f780822f8321c4d
This%20board%20is%20private%20or%20does%20not%20exist/singular: a217ff3f04463b4df8c86adb6f83c6bc
This%20board%20URL%20has%20already%20been%20taken/singular: 1d8b40332a031b5b77a3658e48dd51ca
This%20will%20result%20in%20the%20permanent%20deletion%20of%20all%20data%20associated%20with%20this%20workspace./singular: a31141558af793635c1ddd2fa0a33499
This%20will%20result%20in%20the%20permanent%20deletion%20of%20all%20data%20associated%20with%20your%20account./singular: b49224632bd6c3b7f5e462912aeb1081
This%20workspace%20URL%20has%20already%20been%20taken/singular: b455329e2a71da677acab91d3a00bad6
This%20workspace%20URL%20is%20reserved/singular: 7e47c892b93d4334c1606010c06e875e
This%20workspace%20username%20has%20already%20been%20taken/singular: b7eadb89c615874f416d9658d0428c4c
To%20Do/singular: d60813ea824f373462471e092d136eed
Toggle%20menu/singular: 29dea3e0b6238874f8c7a27619df8e36
Track%20all%20card%20changes%20with%20detailed%20activity%20history./singular: 0d3bac559c71ec4b8734f9f212320de5
Trello/singular: b5131f6488b5a439d58db3e22d6de45b
Trello%20disconnected/singular: 54b24a3e6c9a7eedd8c8d1060ab1175d
Trello%20imports/singular: 6827eca403faa8f89891d17827e72af9
Triaging/singular: 1d40799fcae53a8a27688fdae2a48dee
@@ -413,6 +435,7 @@ checksums:
updated%20the%20title%20to%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: aca196d729401d8adc5c8ca9d558840e
Upgrade/singular: 63c3b52882e0d779859307d672c178c2
Upgrade%20to%20Pro/singular: 972773025e763ddf53273df6d37a729a
Upgrade%20to%20Pro%20(%2429%2Fmonth)/singular: 9a91ea70f5eb55aac96640bf0fd7feff
Upgrade%20to%20Team%20Plan/singular: 40e2ef151266937f8bbe63b3d361f645
Urgent/singular: e79afcd4398e52e10e7e8243a7eecabe
URL%20can%20only%20contain%20letters%2C%20numbers%2C%20and%20hyphens/singular: 875d37d2d920f98ef60a314eda8a5d9a
@@ -437,6 +460,7 @@ checksums:
When%20Trello%20launched%20in%202011%2C%20it%20blew%20everyone%20away%20with%20its%20carefully%20designed%20simplicity%2C%20but%20over%20time%20(and%20atlassification)%20it%20lost%20its%20magic%20and%20grew%20into%20something%20closer%20to%20%22Jira%20Lite%22.%20This%20project%20is%20our%20attempt%20to%20recapture%20the%20original%20magic%20of%20Trello's%20simplicity%2C%20in%20open%20source./singular: 60a55e9bd16c3d1beff1a6c7633c5911
Why%20make%20an%20open%20source%20Trello%3F/singular: 090833338ba8c64ef7a75e0f2615b34b
Workspace/singular: b63ef0e99ee6f7fef6cbe4971ca6cf0f
Workspace%20created%20successfully.%20You%20can%20upgrade%20later%20in%20settings./singular: 8bbaba9f82ff076a08185ada1ee7eb94
Workspace%20deleted/singular: bc5c267ce3f04de94ff1379cf2ca48a3
Workspace%20description/singular: 7159d620b7ef5c5afef734989a5be08b
Workspace%20description%20cannot%20exceed%20280%20characters/singular: d0e83b9a0304d7c36cf8c85d98783ce2
@@ -445,10 +469,12 @@ checksums:
Workspace%20members/singular: 1857292f9567c556c781a23bd7e10544
Workspace%20name/singular: 855614bbf862fd6a9f6a3e76943f92d4
Workspace%20name%20cannot%20exceed%2024%20characters/singular: c8b8f79fb83abf8329cc34c1f51fcfc9
Workspace%20name%20is%20required/singular: b8c5162dd08c4d941bc57f9d0cbee451
Workspace%20name%20must%20be%20at%20least%203%20characters%20long/singular: e448ea97418d44b18b4c21c22b8ba779
Workspace%20name%20updated/singular: 3206ea410ee1ea4182b27ac0d89f92a1
Workspace%20slug%20updated/singular: 527b92711d38cb35b40741df43aef047
Workspace%20URL/singular: f4397a838da0f3a44cbd3ebe408ed6c3
workspace-url/singular: 2d034732ec536f3a2667f956fa50d394
Writing/singular: 4f5f3ee3320c252921f80c42653c5d49
Yearly/singular: 87f43e016c19cb25860f456549a2f431
Yes%2C%20we%20offer%20an%20forever%20free%20plan%20for%20individual%20use.%20No%20restrictions%2C%20no%20paywalls%2C%20no%20limits./singular: 963cee303d0ff13c1719f4575e774fe3

View File

@@ -1,7 +1,7 @@
import type { LinguiConfig } from "@lingui/conf";
const config: LinguiConfig = {
locales: ["en", "fr", "de", "es", "it", "nl"],
locales: ["en", "fr", "de", "es", "it", "nl", "ru"],
sourceLocale: "en",
catalogs: [
{

View File

@@ -52,6 +52,15 @@ const config = {
// instrumentationHook: true,
swcPlugins: [["@lingui/swc-plugin", {}]],
},
async rewrites() {
return [
{
source: "/settings",
destination: "/settings/account",
},
];
},
};
// Only allow external images when OIDC is configured (for OIDC provider avatars)

View File

@@ -59,6 +59,7 @@
"react-dom": "catalog:react18",
"react-hook-form": "^7.51.1",
"react-icons": "^5.5.0",
"react-image-crop": "^11.0.10",
"react-lottie-player": "^1.5.5",
"react-markdown": "^10.1.0",
"superjson": "2.2.1",

View File

@@ -15,7 +15,7 @@ export default function Dropdown({
<div>
<Menu.Button
disabled={disabled}
className="flex h-7 w-7 items-center justify-center rounded-[5px] hover:bg-light-200 dark:hover:bg-dark-200"
className="flex h-7 w-7 items-center justify-center rounded-[5px] hover:bg-light-200 focus:outline-none dark:hover:bg-dark-200"
>
{children}
</Menu.Button>
@@ -30,7 +30,7 @@ export default function Dropdown({
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-30 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
<Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
<div className="flex flex-col">
{items.map((item) => (
<Menu.Item key={item.label}>

View File

@@ -13,7 +13,7 @@ export function LanguageSelector() {
id="language-select"
value={locale}
onChange={(e) => setLocale(e.target.value as any)}
className="mt-8 block w-full max-w-[180px] rounded-lg border-0 bg-light-50 pl-10 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 sm:text-sm"
className="mt-8 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"
>
{availableLocales.map((loc) => (
<option key={loc} value={loc}>

View File

@@ -1,30 +1,106 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useEffect } from "react";
import { env } from "next-runtime-env";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { HiXMark } from "react-icons/hi2";
import {
HiBolt,
HiCheck,
HiCheckBadge,
HiInformationCircle,
HiXMark,
} from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import Button from "~/components/Button";
import Input from "~/components/Input";
import Toggle from "~/components/Toggle";
import { useDebounce } from "~/hooks/useDebounce";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import LoadingSpinner from "./LoadingSpinner";
interface FormValues {
name: string;
}
const schema = z.object({
name: z.string().min(1, { message: t`Workspace name is required` }),
slug: z
.string()
.min(3, {
message: t`URL must be at least 3 characters long`,
})
.max(24, { message: t`URL cannot exceed 24 characters` })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: t`URL can only contain letters, numbers, and hyphens`,
})
.optional()
.or(z.literal("")),
});
type FormValues = z.infer<typeof schema>;
export function NewWorkspaceForm() {
const { closeModal } = useModal();
const { showPopup } = usePopup();
const { switchWorkspace } = useWorkspace();
const { register, handleSubmit } = useForm<FormValues>();
const { switchWorkspace, availableWorkspaces } = useWorkspace();
const {
register,
handleSubmit,
formState: { errors },
watch,
trigger,
clearErrors,
setValue,
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: "",
slug: "",
},
mode: "onSubmit",
});
const utils = api.useUtils();
const hasAvailableWorkspaces = availableWorkspaces.length > 0;
const isCloudEnv = env("NEXT_PUBLIC_KAN_ENV") === "cloud";
const slug = watch("slug");
const [debouncedSlug] = useDebounce(slug, 500);
const isTyping = slug !== debouncedSlug;
// Pro toggle state management
const [isProToggleEnabled, setIsProToggleEnabled] = useState(false);
const [lastAvailableSlug, setLastAvailableSlug] = useState<string>("");
// Validate slug only after debounce
useEffect(() => {
if (isTyping) {
// Clear errors while typing
clearErrors("slug");
} else if (debouncedSlug) {
// Validate after debounce
void trigger("slug");
}
}, [isTyping, debouncedSlug, trigger, clearErrors]);
const checkWorkspaceSlugAvailability =
api.workspace.checkSlugAvailability.useQuery(
{
workspaceSlug: debouncedSlug ?? "",
},
{
enabled: !!debouncedSlug && debouncedSlug.length >= 3 && !errors.slug,
},
);
const isWorkspaceSlugAvailable = checkWorkspaceSlugAvailability.data;
const createWorkspace = api.workspace.create.useMutation({
onSuccess: (values) => {
onSuccess: async (values, variables) => {
if (values.publicId && values.name) {
utils.workspace.all.invalidate();
void utils.workspace.all.invalidate();
switchWorkspace({
publicId: values.publicId,
name: values.name,
@@ -33,6 +109,43 @@ export function NewWorkspaceForm() {
plan: values.plan,
role: "admin",
});
// If in cloud and Pro toggle is enabled, create checkout session for pro
if (env("NEXT_PUBLIC_KAN_ENV") === "cloud" && isProToggleEnabled) {
try {
const response = await fetch(
"/api/stripe/create_checkout_session",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
slug: slug || undefined,
workspacePublicId: values.publicId,
cancelUrl: "/settings/workspace?upgrade=pro",
successUrl: "/boards",
}),
},
);
const data = await response.json();
const url = (data as { url: string }).url;
if (url) {
window.location.href = url;
return; // Don't close modal if redirecting to checkout
}
} catch (error) {
console.error("Error creating checkout session:", error);
showPopup({
header: t`Error upgrading to Pro`,
message: t`Workspace created successfully. You can upgrade later in settings.`,
icon: "warning",
});
}
}
closeModal();
}
},
@@ -51,12 +164,74 @@ export function NewWorkspaceForm() {
if (nameElement) nameElement.focus();
}, []);
const [shouldShowBenefits, setShouldShowBenefits] = useState(false);
const isValidSlug = slug && slug.length >= 3 && !errors.slug;
useEffect(() => {
if (
isCloudEnv &&
!checkWorkspaceSlugAvailability.isPending &&
isValidSlug
) {
const isAvailable = isWorkspaceSlugAvailable?.isAvailable === true;
setShouldShowBenefits(isAvailable);
// Automatically enable Pro toggle when slug is available
if (isAvailable) {
setIsProToggleEnabled(true);
setLastAvailableSlug(slug);
}
}
}, [
isValidSlug,
isWorkspaceSlugAvailable?.isAvailable,
checkWorkspaceSlugAvailability.isPending,
slug,
isCloudEnv,
]);
// Reset benefits when slug becomes invalid
useEffect(() => {
if (!isValidSlug) {
setShouldShowBenefits(false);
}
}, [isValidSlug]);
// Handle Pro toggle changes
const handleProToggleChange = () => {
if (isProToggleEnabled) {
// Turning off: clear the slug and disable Pro
setValue("slug", "");
setIsProToggleEnabled(false);
} else {
// Turning on: restore the last available slug if we have one
if (lastAvailableSlug) {
setValue("slug", lastAvailableSlug);
}
setIsProToggleEnabled(true);
}
};
const showProBenefits = isProToggleEnabled;
const onSubmit = (values: FormValues) => {
// Don't submit if slug is provided but not available
if (values.slug && isWorkspaceSlugAvailable?.isAvailable === false) {
return;
}
createWorkspace.mutate({
name: values.name,
slug: !isCloudEnv && values.slug ? values.slug : undefined,
});
};
const isSlugAvailable =
isValidSlug &&
isWorkspaceSlugAvailable?.isAvailable &&
!isWorkspaceSlugAvailable?.isReserved;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="px-5 pt-5">
@@ -66,7 +241,10 @@ export function NewWorkspaceForm() {
</h2>
<button
type="button"
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
className={twMerge(
"rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300",
!hasAvailableWorkspaces && "invisible",
)}
onClick={(e) => {
e.preventDefault();
closeModal();
@@ -80,6 +258,7 @@ export function NewWorkspaceForm() {
id="workspace-name"
placeholder={t`Workspace name`}
{...register("name")}
errorMessage={errors.name?.message}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -87,10 +266,121 @@ export function NewWorkspaceForm() {
}
}}
/>
<div className="mt-4">
<Input
id="workspace-slug"
placeholder={t`workspace-url`}
{...register("slug")}
className={`${
isSlugAvailable
? "focus:ring-green-500 dark:focus:ring-green-500"
: ""
}`}
errorMessage={
errors.slug?.message ??
(isWorkspaceSlugAvailable?.isAvailable === false &&
isWorkspaceSlugAvailable?.isReserved === false
? t`This workspace URL has already been taken`
: isWorkspaceSlugAvailable?.isReserved
? t`This workspace URL is reserved`
: undefined)
}
prefix={
env("NEXT_PUBLIC_KAN_ENV") === "cloud"
? "kan.bn/"
: `${env("NEXT_PUBLIC_BASE_URL")}/`
}
iconRight={
slug && slug.length >= 3 && !errors.slug ? (
isWorkspaceSlugAvailable?.isAvailable ? (
<HiCheck className="h-4 w-4 text-green-500" />
) : checkWorkspaceSlugAvailability.isPending || isTyping ? (
<LoadingSpinner />
) : null
) : null
}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
await handleSubmit(onSubmit)();
}
}}
/>
{slug && slug.length >= 3 && shouldShowBenefits && (
<div className="mt-2 flex items-center gap-1">
<HiInformationCircle className="h-4 w-4 text-dark-900" />
<p className="text-xs text-gray-500 dark:text-dark-900">
{t`Custom URLs require upgrading to a Pro plan`}
</p>
</div>
)}
</div>
{showProBenefits && (
<div className="mt-6">
<div className="rounded-md bg-light-100 p-3 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900">
<div className="space-y-3">
<div className="flex items-center space-x-3">
<HiCheckBadge className="h-[18px] w-[18px] flex-shrink-0 text-light-1000 dark:text-dark-950" />
<div className="flex items-center space-x-2">
<span className="text-xs text-neutral-900 dark:text-dark-1000">
{t`Unlimited members`}
</span>
<span className="inline-flex items-center rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-600 ring-1 ring-inset ring-emerald-500/20 dark:text-emerald-400 sm:text-[10px]">
{t`Launch offer`}
</span>
</div>
</div>
<div className="flex items-center space-x-3">
<HiCheckBadge className="h-[18px] w-[18px] flex-shrink-0 text-light-1000 dark:text-dark-950" />
<span className="text-xs text-neutral-900 dark:text-dark-1000">
{t`Custom workspace URL`}
</span>
</div>
<div className="flex items-center space-x-3">
<HiCheckBadge className="h-[18px] w-[18px] flex-shrink-0 text-light-1000 dark:text-dark-950" />
<div className="flex items-center space-x-2">
<span className="text-xs text-neutral-900 dark:text-dark-1000">
{t`Board analytics`}
</span>
<span className="inline-flex items-center rounded-full bg-gray-500/10 px-2 py-0.5 text-[10px] font-medium text-gray-600 ring-1 ring-inset ring-gray-500/20 dark:text-gray-400 sm:text-[10px]">
{t`Coming soon`}
</span>
</div>
</div>
</div>
</div>
</div>
)}
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<div
className={twMerge(
"mt-6 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600",
!showProBenefits && "mt-12",
)}
>
{/* Pro Toggle - only show in cloud environment */}
{isCloudEnv && (
<Toggle
isChecked={isProToggleEnabled}
onChange={handleProToggleChange}
label={t`Upgrade to Pro ($29/month)`}
/>
)}
<div>
<Button type="submit" isLoading={createWorkspace.isPending}>
<Button
type="submit"
isLoading={createWorkspace.isPending}
disabled={
createWorkspace.isPending ||
(!!slug &&
(checkWorkspaceSlugAvailability.isPending ||
isWorkspaceSlugAvailable?.isAvailable === false ||
isTyping))
}
>
{t`Create workspace`}
</Button>
</div>

View File

@@ -0,0 +1,148 @@
import Link from "next/link";
import { useRouter } from "next/router";
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
} from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useEffect, useState } from "react";
import {
HiChevronDown,
HiOutlineBanknotes,
HiOutlineCodeBracketSquare,
HiOutlineRectangleGroup,
HiOutlineUser,
} from "react-icons/hi2";
interface SettingsLayoutProps {
children: React.ReactNode;
currentTab: string;
}
export function SettingsLayout({ children, currentTab }: SettingsLayoutProps) {
const router = useRouter();
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const settingsTabs = [
{
key: "account",
icon: <HiOutlineUser />,
label: t`Account`,
condition: true,
},
{
key: "workspace",
icon: <HiOutlineRectangleGroup />,
label: t`Workspace`,
condition: true,
},
{
key: "billing",
label: t`Billing`,
icon: <HiOutlineBanknotes />,
condition: env("NEXT_PUBLIC_KAN_ENV") === "cloud",
},
{
key: "api",
icon: <HiOutlineCodeBracketSquare />,
label: t`API`,
condition: true,
},
{
key: "integrations",
icon: <HiOutlineCodeBracketSquare />,
label: t`Integrations`,
condition: true,
},
];
const availableTabs = settingsTabs.filter((tab) => tab.condition);
// Update selected tab when currentTab prop changes
useEffect(() => {
const tabIndex = availableTabs.findIndex((tab) => tab.key === currentTab);
if (tabIndex !== -1) {
setSelectedTabIndex(tabIndex);
}
}, [currentTab, availableTabs]);
const isTabActive = (tabKey: string) => {
return currentTab === tabKey;
};
return (
<div className="flex h-full w-full flex-col overflow-hidden">
<div className="h-full max-h-[calc(100vdh-3rem)] overflow-y-auto md:max-h-[calc(100vdh-4rem)]">
<div className="m-auto max-w-[1100px] px-5 py-6 md:px-28 md:py-12">
<div className="mb-8 flex w-full justify-between">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
{t`Settings`}
</h1>
</div>
<div className="focus:outline-none">
<div className="sm:hidden">
{/* Mobile dropdown */}
<Listbox
value={selectedTabIndex}
onChange={(index) => {
const tabKey = availableTabs[index]?.key;
if (tabKey) {
void router.push(`/settings/${tabKey}`);
}
}}
>
<div className="relative mb-4">
<ListboxButton className="w-full appearance-none rounded-lg border-0 bg-light-50 py-2 pl-3 pr-10 text-left text-sm text-light-1000 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">
{availableTabs[selectedTabIndex]?.label || "Select a tab"}
<HiChevronDown
aria-hidden="true"
className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-light-900 dark:text-dark-900"
/>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 w-full rounded-lg bg-light-50 py-1 text-sm shadow-lg ring-1 ring-inset ring-light-300 dark:bg-dark-50 dark:ring-dark-300">
{availableTabs.map((tab) => (
<ListboxOption
key={tab.key}
value={availableTabs.indexOf(tab)}
className="relative cursor-pointer select-none py-2 pl-3 pr-9 text-light-1000 dark:text-dark-1000"
>
{tab.label}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
</div>
<div className="hidden sm:block">
<div className="border-b border-gray-200 dark:border-white/10">
<nav
aria-label="Tabs"
className="-mb-px flex space-x-8 focus:outline-none"
>
{availableTabs.map((tab) => (
<Link
key={tab.key}
href={`/settings/${tab.key}`}
className={`whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium transition-colors focus:outline-none ${
isTabActive(tab.key)
? "border-light-1000 text-light-1000 dark:border-dark-1000 dark:text-dark-1000"
: "border-transparent text-light-900 hover:border-light-950 hover:text-light-950 dark:text-dark-900 dark:hover:border-white/20 dark:hover:text-dark-950"
}`}
>
{tab.label}
</Link>
))}
</nav>
</div>
</div>
<div className="focus:outline-none">{children}</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -44,7 +44,7 @@ export default function WorkspaceMenu({
</span>
<span
className={twMerge(
"ml-2 text-sm font-bold text-neutral-900 dark:text-dark-1000",
"ml-2 truncate text-sm font-bold text-neutral-900 dark:text-dark-1000",
isCollapsed && "md:hidden",
)}
>
@@ -87,17 +87,17 @@ export default function WorkspaceMenu({
onClick={() => switchWorkspace(availableWorkspace)}
className="flex w-full items-center justify-between rounded-[5px] px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
>
<div>
<span className="inline-flex h-5 w-5 items-center justify-center rounded-[5px] bg-indigo-700">
<div className="flex min-w-0 flex-1 items-center">
<span className="inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-[5px] bg-indigo-700">
<span className="text-xs font-medium leading-none text-white">
{availableWorkspace.name.charAt(0).toUpperCase()}
</span>
</span>
<span className="ml-2 text-xs font-medium">
<span className="ml-2 truncate text-xs font-medium">
{availableWorkspace.name}
</span>
</div>
{workspace.name === availableWorkspace.name && (
{workspace.publicId === availableWorkspace.publicId && (
<span>
<HiCheck className="h-4 w-4" aria-hidden="true" />
</span>

View File

@@ -34,7 +34,7 @@ const Modal: React.FC<Props> = ({
return (
<Transition.Root show={shouldShow} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -47,7 +47,7 @@ const Modal: React.FC<Props> = ({
<div className="fixed inset-0 bg-light-50 bg-opacity-40 transition-opacity dark:bg-dark-50 dark:bg-opacity-40" />
</Transition.Child>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="fixed inset-0 z-50 w-screen overflow-y-auto">
<div className="flex min-h-full items-start justify-center p-4 text-center sm:items-start sm:p-0">
<Transition.Child
as={Fragment}

View File

@@ -0,0 +1,36 @@
import { useState } from "react";
export function useClipboard({ timeout = 500 } = {}) {
const [error, setError] = useState<string | Error | null | undefined>(null);
const [copied, setCopied] = useState<boolean>(false);
const [copyTimeout, setCopyTimeout] = useState<number | undefined>(undefined);
const handleCopyResult = (hasError: boolean) => {
clearTimeout(copyTimeout);
setCopyTimeout(
setTimeout(() => setCopied(false), timeout) as unknown as number,
);
setCopied(hasError);
};
const copy = (value: string) => {
if ("clipboard" in navigator) {
navigator.clipboard
.writeText(value)
.then(() => handleCopyResult(true))
.catch((err) => setError(err));
} else {
setError(new Error("Error: navigator.clipboard is not supported"));
}
};
const reset = () => {
setError(null);
setCopied(false);
clearTimeout(copyTimeout);
};
return { copy, reset, error, copied };
}

View File

@@ -53,6 +53,10 @@ msgstr "1 Benutzer"
msgid "A powerful, flexible kanban app that helps you organise work, track progress, and deliver results—all in one place."
msgstr "Eine leistungsstarke, flexible Kanban-App, die dir hilft, Arbeit zu organisieren, Fortschritte zu verfolgen und Ergebnisse zu liefern alles an einem Ort."
#: src/components/SettingsLayout.tsx:33
msgid "Account"
msgstr "Konto"
#: src/views/settings/components/DeleteAccountConfirmation.tsx:27
msgid "Account deleted"
msgstr "Konto gelöscht"
@@ -91,8 +95,8 @@ msgstr "Beschreibung hinzufügen... (tippe '/' um Befehle zu öffnen oder '@' um
msgid "Add details..."
msgstr "Details hinzufügen..."
#: src/views/card/components/LabelSelector.tsx:110
#: src/views/card/components/LabelSelector.tsx:118
#: src/views/card/components/LabelSelector.tsx:109
#: src/views/card/components/LabelSelector.tsx:114
msgid "Add label"
msgstr "Label hinzufügen"
@@ -136,6 +140,10 @@ msgstr "hat Label <0>{0}</0> hinzugefügt"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Das Hinzufügen eines neuen Mitglieds kostet zusätzlich {price} ({billingType}) pro Platz."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Passe den quadratischen Zuschnitt an deinen Avatar an."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Administratorrollen"
@@ -148,7 +156,7 @@ msgstr "Alle Systeme funktionieren"
msgid "Already have an account? <0><1>Sign in</1></0>"
msgstr "Du hast bereits ein Konto? <0><1>Anmelden</1></0>"
#: src/views/settings/index.tsx:124
#: src/views/settings/IntegrationsSettings.tsx:61
msgid "An error occurred while disconnecting your Trello account."
msgstr "Beim Trennen deines Trello-Kontos ist ein Fehler aufgetreten."
@@ -156,7 +164,27 @@ msgstr "Beim Trennen deines Trello-Kontos ist ein Fehler aufgetreten."
msgid "An unexpected error occurred. Please try again later."
msgstr "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut."
#: src/views/settings/index.tsx:293
#: src/components/SettingsLayout.tsx:51
msgid "API"
msgstr "API"
#: src/views/settings/components/NewApiKeyModal.tsx:91
msgid "API key created"
msgstr "API-Schlüssel erstellt"
#: src/views/settings/components/NewApiKeyModal.tsx:161
msgid "API key name"
msgstr "API-Schlüsselname"
#: src/views/settings/components/NewApiKeyModal.tsx:25
msgid "API key name cannot exceed 30 characters"
msgstr "Der API-Schlüsselname darf 30 Zeichen nicht überschreiten"
#: src/views/settings/components/NewApiKeyModal.tsx:24
msgid "API key name is required"
msgstr "API-Schlüsselname ist erforderlich"
#: src/views/settings/ApiSettings.tsx:22
msgid "API keys"
msgstr "API-Schlüssel"
@@ -226,12 +254,13 @@ msgstr "jährlich abgerechnet"
msgid "billed monthly"
msgstr "monatlich abgerechnet"
#: src/components/SettingsLayout.tsx:44
#: src/views/boards/components/TemplateBoards.tsx:55
#: src/views/settings/index.tsx:232
#: src/views/settings/BillingSettings.tsx:39
msgid "Billing"
msgstr "Abrechnung"
#: src/views/settings/index.tsx:242
#: src/views/settings/BillingSettings.tsx:49
msgid "Billing portal"
msgstr "Abrechnungsportal"
@@ -243,9 +272,10 @@ msgstr "Blogbeitrag"
msgid "Board"
msgstr "Board"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:80
msgid "Board analytics (coming soon)"
msgstr "Board-Analytik (demnächst verfügbar)"
#: src/components/NewWorkspaceForm.tsx:346
#: src/views/settings/components/UpgradeToProConfirmation.tsx:81
msgid "Board analytics"
msgstr "Board-Analytik"
#: src/views/boards/components/NewBoardForm.tsx:21
msgid "Board name cannot exceed 100 characters"
@@ -285,12 +315,12 @@ msgid "Board visibility updated"
msgstr "Board-Sichtbarkeit aktualisiert"
#: src/components/SideNavigation.tsx:68
#: src/views/boards/index.tsx:27
#: src/views/boards/index.tsx:32
msgid "Boards"
msgstr "Boards"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/boards/index.tsx:23
#: src/views/boards/index.tsx:28
msgid "Boards | {0}"
msgstr "Boards | {0}"
@@ -312,10 +342,11 @@ msgstr "Fehlerbericht"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
#: src/views/settings/components/UpgradeToProConfirmation.tsx:94
#: src/views/settings/components/UpgradeToProConfirmation.tsx:104
msgid "Cancel"
msgstr "Abbrechen"
@@ -327,16 +358,16 @@ msgstr "Karte nicht gefunden"
msgid "Card title"
msgstr "Kartentitel"
#: src/views/settings/AccountSettings.tsx:70
#: src/views/settings/AccountSettings.tsx:80
#: src/views/settings/components/ChangePasswordConfirmation.tsx:109
#: src/views/settings/components/ChangePasswordConfirmation.tsx:178
#: src/views/settings/index.tsx:325
#: src/views/settings/index.tsx:335
msgid "Change Password"
msgstr "Passwort ändern"
#: src/views/settings/index.tsx:224
msgid "Change the language of the app."
msgstr "Ändere die Sprache der App."
#: src/views/settings/AccountSettings.tsx:45
msgid "Change your language preferences."
msgstr "Ändern Sie Ihre Spracheinstellungen."
#: src/views/auth/login/index.tsx:41
#: src/views/auth/signup/index.tsx:67
@@ -356,6 +387,10 @@ msgstr "Filter löschen"
msgid "Click on the link we've sent to {magicLinkRecipient} to sign in."
msgstr "Klicke auf den Link, den wir an {magicLinkRecipient} gesendet haben, um dich anzumelden."
#: src/views/settings/components/NewApiKeyModal.tsx:136
msgid "Close"
msgstr "Schließen"
#: src/views/boards/components/TemplateBoards.tsx:22
msgid "Code Review"
msgstr "Code-Review"
@@ -364,7 +399,9 @@ msgstr "Code-Review"
msgid "Collaborate seamlessly with your team."
msgstr "Arbeite nahtlos mit deinem Team zusammen."
#: src/components/NewWorkspaceForm.tsx:349
#: src/views/home/components/Features.tsx:66
#: src/views/settings/components/UpgradeToProConfirmation.tsx:84
msgid "Coming soon"
msgstr "Demnächst verfügbar"
@@ -398,7 +435,7 @@ msgid "Confirm your new password"
msgstr "Bestätigen Sie Ihr neues Passwort"
#: src/views/boards/components/ImportBoardsForm.tsx:157
#: src/views/settings/index.tsx:269
#: src/views/settings/IntegrationsSettings.tsx:93
msgid "Connect Trello"
msgstr "Trello verbinden"
@@ -406,7 +443,7 @@ msgstr "Trello verbinden"
msgid "Connect your favorite tools to streamline your workflow."
msgstr "Verbinde deine Lieblingstools, um deinen Arbeitsablauf zu optimieren."
#: src/views/settings/index.tsx:256
#: src/views/settings/IntegrationsSettings.tsx:80
msgid "Connect your Trello account to import boards."
msgstr "Verbinde dein Trello-Konto, um Boards zu importieren."
@@ -441,6 +478,10 @@ msgstr "Kontrolliere, wer deine Boards ansehen und bearbeiten kann."
msgid "Create another"
msgstr "Weitere erstellen"
#: src/views/settings/components/NewApiKeyModal.tsx:175
msgid "Create API key"
msgstr "API-Schlüssel erstellen"
#: src/views/boards/components/NewBoardForm.tsx:128
msgid "Create board"
msgstr "Board erstellen"
@@ -465,12 +506,12 @@ msgstr "Liste erstellen"
msgid "Create new board"
msgstr "Neues Board erstellen"
#: src/views/settings/components/CreateAPIKeyForm.tsx:54
#: src/views/settings/ApiSettings.tsx:30
msgid "Create new key"
msgstr "Neuen Schlüssel erstellen"
#: src/views/board/components/NewCardForm.tsx:394
#: src/views/card/components/LabelSelector.tsx:98
#: src/views/card/components/LabelSelector.tsx:97
msgid "Create new label"
msgstr "Neues Label erstellen"
@@ -478,7 +519,7 @@ msgstr "Neues Label erstellen"
msgid "Create new list"
msgstr "Neue Liste erstellen"
#: src/components/NewWorkspaceForm.tsx:94
#: src/components/NewWorkspaceForm.tsx:384
#: src/components/WorkspaceMenu.tsx:116
msgid "Create workspace"
msgstr "Arbeitsbereich erstellen"
@@ -491,6 +532,10 @@ msgstr "hat die Karte erstellt"
msgid "Critical"
msgstr "Kritisch"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Schneide deinen Avatar zu"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Aktuelles Passwort ist erforderlich"
@@ -499,6 +544,11 @@ msgstr "Aktuelles Passwort ist erforderlich"
msgid "Custom domain"
msgstr "Benutzerdefinierte Domain"
#: src/components/NewWorkspaceForm.tsx:315
msgid "Custom URLs require upgrading to a Pro plan"
msgstr "Benutzerdefinierte URLs erfordern ein Upgrade auf einen Pro-Plan"
#: src/components/NewWorkspaceForm.tsx:339
#: src/views/settings/components/UpgradeToProConfirmation.tsx:74
msgid "Custom workspace URL"
msgstr "Benutzerdefinierte Workspace-URL"
@@ -519,9 +569,9 @@ msgstr "Dunkel"
msgid "Delete"
msgstr "Löschen"
#: src/views/settings/AccountSettings.tsx:52
#: src/views/settings/AccountSettings.tsx:62
#: src/views/settings/components/DeleteAccountConfirmation.tsx:96
#: src/views/settings/index.tsx:343
#: src/views/settings/index.tsx:353
msgid "Delete account"
msgstr "Konto löschen"
@@ -542,8 +592,8 @@ msgid "Delete list"
msgstr "Liste löschen"
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:106
#: src/views/settings/index.tsx:306
#: src/views/settings/index.tsx:317
#: src/views/settings/WorkspaceSettings.tsx:96
#: src/views/settings/WorkspaceSettings.tsx:107
msgid "Delete workspace"
msgstr "Workspace löschen"
@@ -569,7 +619,7 @@ msgstr "hat Checklistenelement <0>{0}</0> gelöscht"
msgid "Design"
msgstr "Design"
#: src/views/settings/index.tsx:284
#: src/views/settings/IntegrationsSettings.tsx:108
msgid "Disconnect Trello"
msgstr "Trello trennen"
@@ -577,7 +627,7 @@ msgstr "Trello trennen"
msgid "Discuss and collaborate on cards."
msgstr "Diskutiere und arbeite gemeinsam an Karten."
#: src/views/settings/index.tsx:173
#: src/views/settings/AccountSettings.tsx:35
msgid "Display name"
msgstr "Anzeigename"
@@ -695,7 +745,7 @@ msgstr "Fehler beim Löschen des Labels"
msgid "Error deleting workspace"
msgstr "Fehler beim Löschen des Arbeitsbereichs"
#: src/views/settings/index.tsx:123
#: src/views/settings/IntegrationsSettings.tsx:60
msgid "Error disconnecting Trello"
msgstr "Fehler beim Trennen von Trello"
@@ -708,7 +758,7 @@ msgstr "Fehler beim Einladen des Mitglieds"
msgid "Error updating display name"
msgstr "Fehler beim Aktualisieren des Anzeigenamens"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Fehler beim Aktualisieren des Profilbilds"
@@ -729,8 +779,12 @@ msgstr "Fehler beim Aktualisieren der Workspace-URL"
msgid "Error upgrading subscription"
msgstr "Fehler beim Upgrade des Abonnements"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/components/NewWorkspaceForm.tsx:142
msgid "Error upgrading to Pro"
msgstr "Fehler beim Upgrade auf Pro"
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Fehler beim Hochladen des Profilbilds"
@@ -796,7 +850,7 @@ msgid "Free"
msgstr "Kostenlos"
#: src/views/members/components/InviteMemberForm.tsx:193
#: src/views/members/index.tsx:189
#: src/views/members/index.tsx:208
msgid "Free Plan"
msgstr "Kostenloser Plan"
@@ -905,7 +959,7 @@ msgstr "Ideen"
msgid "Ideas to improve this page..."
msgstr "Ideen zur Verbesserung dieser Seite..."
#: src/views/boards/index.tsx:38
#: src/views/boards/index.tsx:43
msgid "Import"
msgstr "Importieren"
@@ -943,6 +997,7 @@ msgstr "In Bearbeitung"
msgid "Individuals"
msgstr "Einzelpersonen"
#: src/components/SettingsLayout.tsx:57
#: src/views/home/components/Features.tsx:114
msgid "Integrations"
msgstr "Integrationen"
@@ -955,7 +1010,7 @@ msgstr "Vorstellungsgespräch"
msgid "Invalid email address"
msgstr "Ungültige E-Mail-Adresse"
#: src/views/members/index.tsx:201
#: src/views/members/index.tsx:221
msgid "Invite"
msgstr "Einladen"
@@ -963,7 +1018,7 @@ msgstr "Einladen"
msgid "Invite another"
msgstr "Weitere Person einladen"
#: src/views/card/components/MemberSelector.tsx:112
#: src/views/card/components/MemberSelector.tsx:111
#: src/views/members/components/InviteMemberForm.tsx:233
msgid "Invite member"
msgstr "Mitglied einladen"
@@ -999,15 +1054,20 @@ msgstr "Labels"
msgid "Labels & Filters"
msgstr "Labels & Filter"
#: src/views/settings/index.tsx:221
#: src/views/settings/AccountSettings.tsx:42
msgid "Language"
msgstr "Sprache"
#: src/components/NewWorkspaceForm.tsx:332
#: src/views/settings/components/UpgradeToProConfirmation.tsx:67
msgid "Launch offer"
msgstr "Einführungsangebot"
#: src/views/home/components/Pricing.tsx:98
#: src/views/members/index.tsx:191
msgid "Launch offer: Get unlimited members with Pro"
msgstr "Einführungsangebot: Erhalten Sie unbegrenzte Mitglieder mit Pro"
#: src/views/home/components/Pricing.tsx:99
msgid "Launch offer: unlimited seats for just $29/month with Pro"
msgstr "Einführungsangebot: unbegrenzte Plätze für nur $29/Monat mit Pro"
@@ -1076,12 +1136,12 @@ msgstr "Mittlere Priorität"
#: src/views/board/components/Filters.tsx:93
#: src/views/board/components/NewCardForm.tsx:357
#: src/views/card/index.tsx:125
#: src/views/members/index.tsx:172
#: src/views/members/index.tsx:178
msgid "Members"
msgstr "Mitglieder"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/members/index.tsx:168
#: src/views/members/index.tsx:173
msgid "Members | {0}"
msgstr "Mitglieder | {0}"
@@ -1112,10 +1172,14 @@ msgstr "Name"
msgid "Need help?"
msgstr "Brauchst du Hilfe?"
#: src/views/boards/index.tsx:48
#: src/views/boards/index.tsx:53
msgid "New"
msgstr "Neu"
#: src/views/settings/components/NewApiKeyModal.tsx:147
msgid "New API key"
msgstr "Neuer API-Schlüssel"
#: src/views/boards/components/NewBoardForm.tsx:85
msgid "New board"
msgstr "Neues Board"
@@ -1153,7 +1217,7 @@ msgstr "Neues Passwort muss sich vom aktuellen Passwort unterscheiden"
msgid "New Ticket"
msgstr "Neues Ticket"
#: src/components/NewWorkspaceForm.tsx:65
#: src/components/NewWorkspaceForm.tsx:240
msgid "New workspace"
msgstr "Neuer Workspace"
@@ -1189,11 +1253,11 @@ msgstr "Angebot"
msgid "Onboarding"
msgstr "Einarbeitung"
#: src/views/settings/index.tsx:346
#: src/views/settings/AccountSettings.tsx:55
msgid "Once you delete your account, there is no going back. This action cannot be undone."
msgstr "Sobald Sie Ihr Konto löschen, gibt es kein Zurück mehr. Diese Aktion kann nicht rückgängig gemacht werden."
#: src/views/settings/index.tsx:309
#: src/views/settings/WorkspaceSettings.tsx:99
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
msgstr "Sobald Sie Ihren Arbeitsbereich löschen, gibt es kein Zurück mehr. Diese Aktion kann nicht rückgängig gemacht werden."
@@ -1225,11 +1289,15 @@ msgstr "Passwort muss mindestens 8 Zeichen lang sein"
msgid "Passwords do not match"
msgstr "Passwörter stimmen nicht überein"
#: src/views/home/components/Pricing.tsx:101
#: src/views/members/index.tsx:134
msgid "Paused"
msgstr "Pausiert"
#: src/views/home/components/Pricing.tsx:102
msgid "Payment frequency"
msgstr "Zahlungshäufigkeit"
#: src/views/members/index.tsx:129
#: src/views/members/index.tsx:134
msgid "Pending"
msgstr "Ausstehend"
@@ -1262,13 +1330,13 @@ msgstr "Bitte gib einen gültigen Namen ein"
msgid "Please enter a valid password"
msgstr "Bitte gib ein gültiges Passwort ein"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Bitte wähle eine Datei zum Hochladen aus."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:42
#: src/components/NewWorkspaceForm.tsx:155
#: src/views/board/components/NewCardForm.tsx:174
#: src/views/board/components/NewListForm.tsx:78
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
@@ -1283,9 +1351,9 @@ msgstr "Bitte wähle eine Datei zum Hochladen aus."
#: src/views/card/components/DeleteCardConfirmation.tsx:52
#: src/views/card/components/DeleteChecklistConfirmation.tsx:37
#: src/views/card/components/DeleteCommentConfirmation.tsx:45
#: src/views/card/components/LabelSelector.tsx:73
#: src/views/card/components/ListSelector.tsx:53
#: src/views/card/components/MemberSelector.tsx:80
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/NewChecklistForm.tsx:70
#: src/views/card/components/NewChecklistItemForm.tsx:89
#: src/views/card/components/NewCommentForm.tsx:31
@@ -1293,8 +1361,8 @@ msgstr "Bitte wähle eine Datei zum Hochladen aus."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1312,7 +1380,7 @@ msgid "Pricing"
msgstr "Preise"
#: src/views/home/components/Pricing.tsx:56
#: src/views/settings/components/UpgradeToProConfirmation.tsx:86
#: src/views/settings/components/UpgradeToProConfirmation.tsx:91
msgid "Priority email support"
msgstr "Prioritäts-E-Mail-Support"
@@ -1324,7 +1392,7 @@ msgstr "Datenschutzrichtlinie"
msgid "Private"
msgstr "Privat"
#: src/views/members/index.tsx:186
#: src/views/members/index.tsx:205
msgid "Pro Plan"
msgstr "Pro-Plan"
@@ -1332,11 +1400,11 @@ msgstr "Pro-Plan"
msgid "Pro Plan ∞"
msgstr "Pro-Plan ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Profilbild aktualisiert"
#: src/views/settings/index.tsx:168
#: src/views/settings/AccountSettings.tsx:29
msgid "Profile picture"
msgstr "Profilbild"
@@ -1372,7 +1440,7 @@ msgstr "Remote"
msgid "Remove"
msgstr "Entfernen"
#: src/views/members/index.tsx:143
#: src/views/members/index.tsx:148
msgid "Remove member"
msgstr "Mitglied entfernen"
@@ -1419,16 +1487,12 @@ msgstr "Ressourcen"
msgid "Review"
msgstr "Überprüfung"
#: src/views/settings/components/CreateAPIKeyForm.tsx:49
msgid "Revoke"
msgstr "Widerrufen"
#: src/views/home/components/Footer.tsx:36
#: src/views/home/components/Header.tsx:13
msgid "Roadmap"
msgstr "Roadmap"
#: src/views/members/index.tsx:223
#: src/views/members/index.tsx:243
msgid "Role"
msgstr "Rolle"
@@ -1437,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Auf eigener Infrastruktur betreiben"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Speichern"
@@ -1476,15 +1541,30 @@ msgstr "Feedback senden"
msgid "Senior"
msgstr "Senior"
#: src/components/SettingsLayout.tsx:82
#: src/components/SideNavigation.tsx:78
#: src/views/settings/index.tsx:162
msgid "Settings"
msgstr "Einstellungen"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/settings/index.tsx:158
msgid "Settings | {0}"
msgstr "Einstellungen | {0}"
#: src/views/settings/AccountSettings.tsx:25
msgid "Settings | Account"
msgstr "Einstellungen | Konto"
#: src/views/settings/ApiSettings.tsx:18
msgid "Settings | API"
msgstr "Einstellungen | API"
#: src/views/settings/BillingSettings.tsx:35
msgid "Settings | Billing"
msgstr "Einstellungen | Abrechnung"
#: src/views/settings/IntegrationsSettings.tsx:69
msgid "Settings | Integrations"
msgstr "Einstellungen | Integrationen"
#: src/views/settings/WorkspaceSettings.tsx:54
msgid "Settings | Workspace"
msgstr "Einstellungen | Arbeitsbereich"
#: src/views/home/components/Header.tsx:100
#: src/views/home/components/Header.tsx:138
@@ -1550,7 +1630,7 @@ msgid "System"
msgstr "System"
#: src/views/members/components/InviteMemberForm.tsx:182
#: src/views/members/index.tsx:188
#: src/views/members/index.tsx:207
msgid "Team Plan"
msgstr "Team-Plan"
@@ -1602,6 +1682,10 @@ msgstr "Sie werden keinen Zugriff mehr auf diesen Workspace haben."
msgid "This action can't be undone."
msgstr "Diese Aktion kann nicht rückgängig gemacht werden."
#: src/views/settings/components/NewApiKeyModal.tsx:129
msgid "This API key will only be shown once. Please save it in a secure location."
msgstr "Dieser API-Schlüssel wird nur einmal angezeigt. Bitte speichern Sie ihn an einem sicheren Ort."
#: src/views/public/board/index.tsx:151
msgid "This board is private or does not exist"
msgstr "Dieses Board ist privat oder existiert nicht"
@@ -1618,6 +1702,14 @@ msgstr "Dies führt zur permanenten Löschung aller mit diesem Workspace verbund
msgid "This will result in the permanent deletion of all data associated with your account."
msgstr "Dies führt zur permanenten Löschung aller mit deinem Konto verbundenen Daten."
#: src/components/NewWorkspaceForm.tsx:284
msgid "This workspace URL has already been taken"
msgstr "Diese Workspace-URL ist bereits vergeben"
#: src/components/NewWorkspaceForm.tsx:286
msgid "This workspace URL is reserved"
msgstr "Diese Workspace-URL ist reserviert"
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:123
msgid "This workspace username has already been taken"
msgstr "Dieser Workspace-Benutzername ist bereits vergeben"
@@ -1635,7 +1727,11 @@ msgstr "Menü umschalten"
msgid "Track all card changes with detailed activity history."
msgstr "Verfolge alle kartenänderungen mit detaillierter aktivitätshistorie."
#: src/views/settings/index.tsx:116
#: src/views/settings/IntegrationsSettings.tsx:73
msgid "Trello"
msgstr "Trello"
#: src/views/settings/IntegrationsSettings.tsx:53
msgid "Trello disconnected"
msgstr "Trello getrennt"
@@ -1667,7 +1763,7 @@ msgstr "Checkliste kann nicht erstellt werden"
msgid "Unable to create list"
msgstr "Liste konnte nicht erstellt werden"
#: src/components/NewWorkspaceForm.tsx:41
#: src/components/NewWorkspaceForm.tsx:154
msgid "Unable to create workspace"
msgstr "Workspace konnte nicht erstellt werden"
@@ -1720,16 +1816,16 @@ msgstr "Checklistenelement kann nicht aktualisiert werden"
msgid "Unable to update comment"
msgstr "Kommentar konnte nicht aktualisiert werden"
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/LabelSelector.tsx:71
msgid "Unable to update labels"
msgstr "Labels konnten nicht aktualisiert werden"
#: src/views/board/index.tsx:133
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/ListSelector.tsx:51
msgid "Unable to update list"
msgstr "Liste konnte nicht aktualisiert werden"
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/MemberSelector.tsx:78
msgid "Unable to update members"
msgstr "Mitglieder konnten nicht aktualisiert werden"
@@ -1766,14 +1862,15 @@ msgstr "Unbegrenzte kommentare"
msgid "Unlimited lists"
msgstr "Unbegrenzte listen"
#: src/components/NewWorkspaceForm.tsx:329
#: src/views/settings/components/UpgradeToProConfirmation.tsx:64
msgid "Unlimited members"
msgstr "Unbegrenzte Mitglieder"
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
#: src/views/settings/components/UpdateDisplayNameForm.tsx:79
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:90
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:82
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:154
msgid "Update"
msgstr "Aktualisieren"
@@ -1799,15 +1896,19 @@ msgstr "hat den Titel aktualisiert"
msgid "updated the title to <0>{0}</0>"
msgstr "hat den Titel in <0>{0}</0> geändert"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:99
#: src/views/settings/components/UpgradeToProConfirmation.tsx:109
msgid "Upgrade"
msgstr "Upgrade"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:52
#: src/views/settings/index.tsx:213
#: src/views/settings/WorkspaceSettings.tsx:89
msgid "Upgrade to Pro"
msgstr "Upgrade auf Pro"
#: src/components/NewWorkspaceForm.tsx:369
msgid "Upgrade to Pro ($29/month)"
msgstr "Upgrade auf Pro ($29/Monat)"
#: src/views/members/components/InviteMemberForm.tsx:224
msgid "Upgrade to Team Plan"
msgstr "Upgrade auf Team-Plan"
@@ -1817,14 +1918,17 @@ msgstr "Upgrade auf Team-Plan"
msgid "Urgent"
msgstr "Dringend"
#: src/components/NewWorkspaceForm.tsx:35
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:36
msgid "URL can only contain letters, numbers, and hyphens"
msgstr "URL darf nur Buchstaben, Zahlen und Bindestriche enthalten"
#: src/components/NewWorkspaceForm.tsx:33
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:34
msgid "URL cannot exceed 24 characters"
msgstr "URL darf nicht länger als 24 Zeichen sein"
#: src/components/NewWorkspaceForm.tsx:31
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:32
msgid "URL must be at least 3 characters long"
msgstr "URL muss mindestens 3 Zeichen lang sein"
@@ -1833,7 +1937,7 @@ msgstr "URL muss mindestens 3 Zeichen lang sein"
msgid "Use template"
msgstr "Vorlage verwenden"
#: src/views/members/index.tsx:217
#: src/views/members/index.tsx:237
msgid "User"
msgstr "Benutzer"
@@ -1845,11 +1949,11 @@ msgstr "Benutzer ist bereits Mitglied dieses Arbeitsbereichs"
msgid "Video"
msgstr "Video"
#: src/views/settings/index.tsx:296
#: src/views/settings/ApiSettings.tsx:25
msgid "View and manage your API keys."
msgstr "API-Schlüssel anzeigen und verwalten."
#: src/views/settings/index.tsx:235
#: src/views/settings/BillingSettings.tsx:42
msgid "View and manage your billing and subscription."
msgstr "Verwalte deine Abrechnung und dein Abonnement."
@@ -1901,15 +2005,20 @@ msgstr "Als Trello 2011 auf den markt kam, beeindruckte es alle mit seiner sorgf
msgid "Why make an open source Trello?"
msgstr "Warum ein open source Trello entwickeln?"
#: src/components/SettingsLayout.tsx:39
#: src/views/board/index.tsx:331
msgid "Workspace"
msgstr "Arbeitsbereich"
#: src/components/NewWorkspaceForm.tsx:143
msgid "Workspace created successfully. You can upgrade later in settings."
msgstr "Workspace erfolgreich erstellt. Du kannst später in den Einstellungen upgraden."
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:26
msgid "Workspace deleted"
msgstr "Workspace gelöscht"
#: src/views/settings/index.tsx:199
#: src/views/settings/WorkspaceSettings.tsx:75
msgid "Workspace description"
msgstr "Workspace-Beschreibung"
@@ -1930,8 +2039,8 @@ msgstr "Workspace-Beschreibung aktualisiert"
msgid "Workspace members"
msgstr "Workspace-mitglieder"
#: src/components/NewWorkspaceForm.tsx:81
#: src/views/settings/index.tsx:180
#: src/components/NewWorkspaceForm.tsx:259
#: src/views/settings/WorkspaceSettings.tsx:58
msgid "Workspace name"
msgstr "Name des Workspaces"
@@ -1939,6 +2048,10 @@ msgstr "Name des Workspaces"
msgid "Workspace name cannot exceed 24 characters"
msgstr "Der Workspace-Name darf nicht länger als 24 Zeichen sein"
#: src/components/NewWorkspaceForm.tsx:27
msgid "Workspace name is required"
msgstr "Workspace-Name ist erforderlich"
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:14
msgid "Workspace name must be at least 3 characters long"
msgstr "Der Workspace-Name muss mindestens 3 Zeichen lang sein"
@@ -1951,10 +2064,14 @@ msgstr "Workspace-Name aktualisiert"
msgid "Workspace slug updated"
msgstr "Workspace-Slug aktualisiert"
#: src/views/settings/index.tsx:189
#: src/views/settings/WorkspaceSettings.tsx:66
msgid "Workspace URL"
msgstr "Workspace-URL"
#: src/components/NewWorkspaceForm.tsx:273
msgid "workspace-url"
msgstr "workspace-url"
#: src/views/boards/components/TemplateBoards.tsx:30
msgid "Writing"
msgstr "Schreiben"
@@ -1967,7 +2084,7 @@ msgstr "Jährlich"
msgid "Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits."
msgstr "Ja, wir bieten einen dauerhaft kostenlosen plan für die individuelle nutzung an. Keine einschränkungen, keine paywalls, keine limits."
#: src/views/settings/index.tsx:328
#: src/views/settings/AccountSettings.tsx:73
msgid "You are about to change your password."
msgstr "Sie sind dabei, Ihr Passwort zu ändern."
@@ -2011,15 +2128,15 @@ msgstr "Dein Anzeigename wurde aktualisiert."
msgid "Your password has been changed."
msgstr "Ihr Passwort wurde geändert."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Dein Profilbild wurde aktualisiert."
#: src/views/settings/index.tsx:117
#: src/views/settings/IntegrationsSettings.tsx:54
msgid "Your Trello account has been disconnected."
msgstr "Dein Trello-Konto wurde getrennt."
#: src/views/settings/index.tsx:278
#: src/views/settings/IntegrationsSettings.tsx:102
msgid "Your Trello account is connected."
msgstr "Dein Trello-Konto ist verbunden."

File diff suppressed because one or more lines are too long

View File

@@ -65,6 +65,10 @@ msgstr "1 user"
msgid "A powerful, flexible kanban app that helps you organise work, track progress, and deliver results—all in one place."
msgstr "A powerful, flexible kanban app that helps you organise work, track progress, and deliver results—all in one place."
#: src/components/SettingsLayout.tsx:33
msgid "Account"
msgstr "Account"
#: src/views/settings/components/DeleteAccountConfirmation.tsx:27
msgid "Account deleted"
msgstr "Account deleted"
@@ -107,8 +111,8 @@ msgstr "Add description... (type '/' to open commands or '@' to mention)"
msgid "Add details..."
msgstr "Add details..."
#: src/views/card/components/LabelSelector.tsx:110
#: src/views/card/components/LabelSelector.tsx:118
#: src/views/card/components/LabelSelector.tsx:109
#: src/views/card/components/LabelSelector.tsx:114
msgid "Add label"
msgstr "Add label"
@@ -156,6 +160,10 @@ msgstr "added label <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Adding a new member will cost an additional {price} ({billingType}) per seat."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Adjust the square crop to fit your avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Admin roles"
@@ -172,7 +180,7 @@ msgstr "All systems operational"
msgid "Already have an account? <0><1>Sign in</1></0>"
msgstr "Already have an account? <0><1>Sign in</1></0>"
#: src/views/settings/index.tsx:124
#: src/views/settings/IntegrationsSettings.tsx:61
msgid "An error occurred while disconnecting your Trello account."
msgstr "An error occurred while disconnecting your Trello account."
@@ -180,7 +188,27 @@ msgstr "An error occurred while disconnecting your Trello account."
msgid "An unexpected error occurred. Please try again later."
msgstr "An unexpected error occurred. Please try again later."
#: src/views/settings/index.tsx:293
#: src/components/SettingsLayout.tsx:51
msgid "API"
msgstr "API"
#: src/views/settings/components/NewApiKeyModal.tsx:91
msgid "API key created"
msgstr "API key created"
#: src/views/settings/components/NewApiKeyModal.tsx:161
msgid "API key name"
msgstr "API key name"
#: src/views/settings/components/NewApiKeyModal.tsx:25
msgid "API key name cannot exceed 30 characters"
msgstr "API key name cannot exceed 30 characters"
#: src/views/settings/components/NewApiKeyModal.tsx:24
msgid "API key name is required"
msgstr "API key name is required"
#: src/views/settings/ApiSettings.tsx:22
msgid "API keys"
msgstr "API keys"
@@ -262,12 +290,13 @@ msgstr "billed annually"
msgid "billed monthly"
msgstr "billed monthly"
#: src/components/SettingsLayout.tsx:44
#: src/views/boards/components/TemplateBoards.tsx:55
#: src/views/settings/index.tsx:232
#: src/views/settings/BillingSettings.tsx:39
msgid "Billing"
msgstr "Billing"
#: src/views/settings/index.tsx:242
#: src/views/settings/BillingSettings.tsx:49
msgid "Billing portal"
msgstr "Billing portal"
@@ -279,9 +308,15 @@ msgstr "Blog Post"
msgid "Board"
msgstr "Board"
#: src/components/NewWorkspaceForm.tsx:346
#: src/views/settings/components/UpgradeToProConfirmation.tsx:81
msgid "Board analytics"
msgstr "Board analytics"
#: src/components/NewWorkspaceForm.tsx:310
#: src/views/settings/components/UpgradeToProConfirmation.tsx:80
msgid "Board analytics (coming soon)"
msgstr "Board analytics (coming soon)"
#~ msgid "Board analytics (coming soon)"
#~ msgstr "Board analytics (coming soon)"
#: src/views/boards/components/NewBoardForm.tsx:21
msgid "Board name cannot exceed 100 characters"
@@ -325,12 +360,12 @@ msgid "Board visibility updated"
msgstr "Board visibility updated"
#: src/components/SideNavigation.tsx:68
#: src/views/boards/index.tsx:27
#: src/views/boards/index.tsx:32
msgid "Boards"
msgstr "Boards"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/boards/index.tsx:23
#: src/views/boards/index.tsx:28
msgid "Boards | {0}"
msgstr "Boards | {0}"
@@ -356,10 +391,11 @@ msgstr "Bug Report"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
#: src/views/settings/components/UpgradeToProConfirmation.tsx:94
#: src/views/settings/components/UpgradeToProConfirmation.tsx:104
msgid "Cancel"
msgstr "Cancel"
@@ -371,16 +407,20 @@ msgstr "Card not found"
msgid "Card title"
msgstr "Card title"
#: src/views/settings/AccountSettings.tsx:70
#: src/views/settings/AccountSettings.tsx:80
#: src/views/settings/components/ChangePasswordConfirmation.tsx:109
#: src/views/settings/components/ChangePasswordConfirmation.tsx:178
#: src/views/settings/index.tsx:325
#: src/views/settings/index.tsx:335
msgid "Change Password"
msgstr "Change Password"
#: src/views/settings/index.tsx:224
msgid "Change the language of the app."
msgstr "Change the language of the app."
#: src/views/settings/index.tsx:227
#~ msgid "Change the language of the app."
#~ msgstr "Change the language of the app."
#: src/views/settings/AccountSettings.tsx:45
msgid "Change your language preferences."
msgstr "Change your language preferences."
#: src/views/auth/login/index.tsx:41
#: src/views/auth/signup/index.tsx:67
@@ -400,6 +440,10 @@ msgstr "Clear filters"
msgid "Click on the link we've sent to {magicLinkRecipient} to sign in."
msgstr "Click on the link we've sent to {magicLinkRecipient} to sign in."
#: src/views/settings/components/NewApiKeyModal.tsx:136
msgid "Close"
msgstr "Close"
#: src/views/boards/components/TemplateBoards.tsx:22
msgid "Code Review"
msgstr "Code Review"
@@ -408,7 +452,9 @@ msgstr "Code Review"
msgid "Collaborate seamlessly with your team."
msgstr "Collaborate seamlessly with your team."
#: src/components/NewWorkspaceForm.tsx:349
#: src/views/home/components/Features.tsx:66
#: src/views/settings/components/UpgradeToProConfirmation.tsx:84
msgid "Coming soon"
msgstr "Coming soon"
@@ -446,7 +492,7 @@ msgid "Confirm your new password"
msgstr "Confirm your new password"
#: src/views/boards/components/ImportBoardsForm.tsx:157
#: src/views/settings/index.tsx:269
#: src/views/settings/IntegrationsSettings.tsx:93
msgid "Connect Trello"
msgstr "Connect Trello"
@@ -454,7 +500,7 @@ msgstr "Connect Trello"
msgid "Connect your favorite tools to streamline your workflow."
msgstr "Connect your favorite tools to streamline your workflow."
#: src/views/settings/index.tsx:256
#: src/views/settings/IntegrationsSettings.tsx:80
msgid "Connect your Trello account to import boards."
msgstr "Connect your Trello account to import boards."
@@ -489,9 +535,9 @@ msgstr "Control who can view and edit your boards."
msgid "Create another"
msgstr "Create another"
#: src/views/settings/index.tsx:238
#~ msgid "Create API key"
#~ msgstr "Create API key"
#: src/views/settings/components/NewApiKeyModal.tsx:175
msgid "Create API key"
msgstr "Create API key"
#: src/views/settings/index.tsx:232
#~ msgid "Create API keys to access the Kan API."
@@ -521,12 +567,12 @@ msgstr "Create list"
msgid "Create new board"
msgstr "Create new board"
#: src/views/settings/components/CreateAPIKeyForm.tsx:54
#: src/views/settings/ApiSettings.tsx:30
msgid "Create new key"
msgstr "Create new key"
#: src/views/board/components/NewCardForm.tsx:394
#: src/views/card/components/LabelSelector.tsx:98
#: src/views/card/components/LabelSelector.tsx:97
msgid "Create new label"
msgstr "Create new label"
@@ -534,7 +580,11 @@ msgstr "Create new label"
msgid "Create new list"
msgstr "Create new list"
#: src/components/NewWorkspaceForm.tsx:94
#: src/components/NewWorkspaceForm.tsx:337
#~ msgid "Create Pro workspace"
#~ msgstr "Create Pro workspace"
#: src/components/NewWorkspaceForm.tsx:384
#: src/components/WorkspaceMenu.tsx:116
msgid "Create workspace"
msgstr "Create workspace"
@@ -547,6 +597,10 @@ msgstr "created the card"
msgid "Critical"
msgstr "Critical"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Crop your avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Current password is required"
@@ -559,6 +613,15 @@ msgstr "Custom domain"
#~ msgid "Custom URLs are a premium feature. You'll be directed to upgrade your account."
#~ msgstr "Custom URLs are a premium feature. You'll be directed to upgrade your account."
#: src/components/NewWorkspaceForm.tsx:280
#~ msgid "Custom URLs require Pro plan ($29/month)"
#~ msgstr "Custom URLs require Pro plan ($29/month)"
#: src/components/NewWorkspaceForm.tsx:315
msgid "Custom URLs require upgrading to a Pro plan"
msgstr "Custom URLs require upgrading to a Pro plan"
#: src/components/NewWorkspaceForm.tsx:339
#: src/views/settings/components/UpgradeToProConfirmation.tsx:74
msgid "Custom workspace URL"
msgstr "Custom workspace URL"
@@ -579,9 +642,9 @@ msgstr "Dark"
msgid "Delete"
msgstr "Delete"
#: src/views/settings/AccountSettings.tsx:52
#: src/views/settings/AccountSettings.tsx:62
#: src/views/settings/components/DeleteAccountConfirmation.tsx:96
#: src/views/settings/index.tsx:343
#: src/views/settings/index.tsx:353
msgid "Delete account"
msgstr "Delete account"
@@ -602,8 +665,8 @@ msgid "Delete list"
msgstr "Delete list"
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:106
#: src/views/settings/index.tsx:306
#: src/views/settings/index.tsx:317
#: src/views/settings/WorkspaceSettings.tsx:96
#: src/views/settings/WorkspaceSettings.tsx:107
msgid "Delete workspace"
msgstr "Delete workspace"
@@ -629,7 +692,7 @@ msgstr "deleted checklist item <0>{0}</0>"
msgid "Design"
msgstr "Design"
#: src/views/settings/index.tsx:284
#: src/views/settings/IntegrationsSettings.tsx:108
msgid "Disconnect Trello"
msgstr "Disconnect Trello"
@@ -637,7 +700,7 @@ msgstr "Disconnect Trello"
msgid "Discuss and collaborate on cards."
msgstr "Discuss and collaborate on cards."
#: src/views/settings/index.tsx:173
#: src/views/settings/AccountSettings.tsx:35
msgid "Display name"
msgstr "Display name"
@@ -755,7 +818,7 @@ msgstr "Error deleting label"
msgid "Error deleting workspace"
msgstr "Error deleting workspace"
#: src/views/settings/index.tsx:123
#: src/views/settings/IntegrationsSettings.tsx:60
msgid "Error disconnecting Trello"
msgstr "Error disconnecting Trello"
@@ -768,7 +831,7 @@ msgstr "Error inviting member"
msgid "Error updating display name"
msgstr "Error updating display name"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Error updating profile image"
@@ -789,8 +852,12 @@ msgstr "Error updating workspace URL"
msgid "Error upgrading subscription"
msgstr "Error upgrading subscription"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/components/NewWorkspaceForm.tsx:142
msgid "Error upgrading to Pro"
msgstr "Error upgrading to Pro"
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Error uploading profile image"
@@ -857,7 +924,7 @@ msgid "Free"
msgstr "Free"
#: src/views/members/components/InviteMemberForm.tsx:193
#: src/views/members/index.tsx:189
#: src/views/members/index.tsx:208
msgid "Free Plan"
msgstr "Free Plan"
@@ -970,7 +1037,7 @@ msgstr "Ideas to improve this page..."
#~ msgid "Ideas, Research, Planning, Execution, Review, Next Steps, Complete"
#~ msgstr "Ideas, Research, Planning, Execution, Review, Next Steps, Complete"
#: src/views/boards/index.tsx:38
#: src/views/boards/index.tsx:43
msgid "Import"
msgstr "Import"
@@ -1008,6 +1075,7 @@ msgstr "In Progress"
msgid "Individuals"
msgstr "Individuals"
#: src/components/SettingsLayout.tsx:57
#: src/views/home/components/Features.tsx:114
msgid "Integrations"
msgstr "Integrations"
@@ -1020,7 +1088,7 @@ msgstr "Interviewing"
msgid "Invalid email address"
msgstr "Invalid email address"
#: src/views/members/index.tsx:201
#: src/views/members/index.tsx:221
msgid "Invite"
msgstr "Invite"
@@ -1028,7 +1096,7 @@ msgstr "Invite"
msgid "Invite another"
msgstr "Invite another"
#: src/views/card/components/MemberSelector.tsx:112
#: src/views/card/components/MemberSelector.tsx:111
#: src/views/members/components/InviteMemberForm.tsx:233
msgid "Invite member"
msgstr "Invite member"
@@ -1064,15 +1132,20 @@ msgstr "Labels"
msgid "Labels & Filters"
msgstr "Labels & Filters"
#: src/views/settings/index.tsx:221
#: src/views/settings/AccountSettings.tsx:42
msgid "Language"
msgstr "Language"
#: src/components/NewWorkspaceForm.tsx:332
#: src/views/settings/components/UpgradeToProConfirmation.tsx:67
msgid "Launch offer"
msgstr "Launch offer"
#: src/views/home/components/Pricing.tsx:98
#: src/views/members/index.tsx:191
msgid "Launch offer: Get unlimited members with Pro"
msgstr "Launch offer: Get unlimited members with Pro"
#: src/views/home/components/Pricing.tsx:99
msgid "Launch offer: unlimited seats for just $29/month with Pro"
msgstr "Launch offer: unlimited seats for just $29/month with Pro"
@@ -1141,12 +1214,12 @@ msgstr "Medium Priority"
#: src/views/board/components/Filters.tsx:93
#: src/views/board/components/NewCardForm.tsx:357
#: src/views/card/index.tsx:125
#: src/views/members/index.tsx:172
#: src/views/members/index.tsx:178
msgid "Members"
msgstr "Members"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/members/index.tsx:168
#: src/views/members/index.tsx:173
msgid "Members | {0}"
msgstr "Members | {0}"
@@ -1172,6 +1245,10 @@ msgstr "moved the card from <0>{0}</0> to<1>{1}</1>"
msgid "moved the card to another list"
msgstr "moved the card to another list"
#: src/components/NewWorkspaceForm.tsx:273
#~ msgid "my-workspace-name"
#~ msgstr "my-workspace-name"
#: src/components/LabelForm.tsx:135
#: src/views/boards/components/NewBoardForm.tsx:99
msgid "Name"
@@ -1181,10 +1258,14 @@ msgstr "Name"
msgid "Need help?"
msgstr "Need help?"
#: src/views/boards/index.tsx:48
#: src/views/boards/index.tsx:53
msgid "New"
msgstr "New"
#: src/views/settings/components/NewApiKeyModal.tsx:147
msgid "New API key"
msgstr "New API key"
#: src/views/boards/components/NewBoardForm.tsx:85
msgid "New board"
msgstr "New board"
@@ -1226,7 +1307,7 @@ msgstr "New Ticket"
#~ msgid "New Ticket, Triaging, In Progress, Awaiting Customer, Resolution, Done"
#~ msgstr "New Ticket, Triaging, In Progress, Awaiting Customer, Resolution, Done"
#: src/components/NewWorkspaceForm.tsx:65
#: src/components/NewWorkspaceForm.tsx:240
msgid "New workspace"
msgstr "New workspace"
@@ -1262,11 +1343,11 @@ msgstr "Offer"
msgid "Onboarding"
msgstr "Onboarding"
#: src/views/settings/index.tsx:346
#: src/views/settings/AccountSettings.tsx:55
msgid "Once you delete your account, there is no going back. This action cannot be undone."
msgstr "Once you delete your account, there is no going back. This action cannot be undone."
#: src/views/settings/index.tsx:309
#: src/views/settings/WorkspaceSettings.tsx:99
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
msgstr "Once you delete your workspace, there is no going back. This action cannot be undone."
@@ -1298,11 +1379,15 @@ msgstr "Password must be at least 8 characters"
msgid "Passwords do not match"
msgstr "Passwords do not match"
#: src/views/home/components/Pricing.tsx:101
#: src/views/members/index.tsx:134
msgid "Paused"
msgstr "Paused"
#: src/views/home/components/Pricing.tsx:102
msgid "Payment frequency"
msgstr "Payment frequency"
#: src/views/members/index.tsx:129
#: src/views/members/index.tsx:134
msgid "Pending"
msgstr "Pending"
@@ -1335,13 +1420,13 @@ msgstr "Please enter a valid name"
msgid "Please enter a valid password"
msgstr "Please enter a valid password"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Please select a file to upload."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:42
#: src/components/NewWorkspaceForm.tsx:155
#: src/views/board/components/NewCardForm.tsx:174
#: src/views/board/components/NewListForm.tsx:78
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
@@ -1356,9 +1441,9 @@ msgstr "Please select a file to upload."
#: src/views/card/components/DeleteCardConfirmation.tsx:52
#: src/views/card/components/DeleteChecklistConfirmation.tsx:37
#: src/views/card/components/DeleteCommentConfirmation.tsx:45
#: src/views/card/components/LabelSelector.tsx:73
#: src/views/card/components/ListSelector.tsx:53
#: src/views/card/components/MemberSelector.tsx:80
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/NewChecklistForm.tsx:70
#: src/views/card/components/NewChecklistItemForm.tsx:89
#: src/views/card/components/NewCommentForm.tsx:31
@@ -1366,8 +1451,8 @@ msgstr "Please select a file to upload."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1385,7 +1470,7 @@ msgid "Pricing"
msgstr "Pricing"
#: src/views/home/components/Pricing.tsx:56
#: src/views/settings/components/UpgradeToProConfirmation.tsx:86
#: src/views/settings/components/UpgradeToProConfirmation.tsx:91
msgid "Priority email support"
msgstr "Priority email support"
@@ -1397,7 +1482,7 @@ msgstr "Privacy policy"
msgid "Private"
msgstr "Private"
#: src/views/members/index.tsx:186
#: src/views/members/index.tsx:205
msgid "Pro Plan"
msgstr "Pro Plan"
@@ -1405,11 +1490,11 @@ msgstr "Pro Plan"
msgid "Pro Plan ∞"
msgstr "Pro Plan ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Profile image updated"
#: src/views/settings/index.tsx:168
#: src/views/settings/AccountSettings.tsx:29
msgid "Profile picture"
msgstr "Profile picture"
@@ -1445,7 +1530,7 @@ msgstr "Remote"
msgid "Remove"
msgstr "Remove"
#: src/views/members/index.tsx:143
#: src/views/members/index.tsx:148
msgid "Remove member"
msgstr "Remove member"
@@ -1497,15 +1582,15 @@ msgid "Review"
msgstr "Review"
#: src/views/settings/components/CreateAPIKeyForm.tsx:49
msgid "Revoke"
msgstr "Revoke"
#~ msgid "Revoke"
#~ msgstr "Revoke"
#: src/views/home/components/Footer.tsx:36
#: src/views/home/components/Header.tsx:13
msgid "Roadmap"
msgstr "Roadmap"
#: src/views/members/index.tsx:223
#: src/views/members/index.tsx:243
msgid "Role"
msgstr "Role"
@@ -1514,6 +1599,7 @@ msgid "Run on your own infrastructure"
msgstr "Run on your own infrastructure"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Save"
@@ -1557,15 +1643,34 @@ msgstr "Send feedback"
msgid "Senior"
msgstr "Senior"
#: src/components/SettingsLayout.tsx:82
#: src/components/SideNavigation.tsx:78
#: src/views/settings/index.tsx:162
msgid "Settings"
msgstr "Settings"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/settings/index.tsx:158
msgid "Settings | {0}"
msgstr "Settings | {0}"
#: src/views/settings/index.tsx:161
#~ msgid "Settings | {0}"
#~ msgstr "Settings | {0}"
#: src/views/settings/AccountSettings.tsx:25
msgid "Settings | Account"
msgstr "Settings | Account"
#: src/views/settings/ApiSettings.tsx:18
msgid "Settings | API"
msgstr "Settings | API"
#: src/views/settings/BillingSettings.tsx:35
msgid "Settings | Billing"
msgstr "Settings | Billing"
#: src/views/settings/IntegrationsSettings.tsx:69
msgid "Settings | Integrations"
msgstr "Settings | Integrations"
#: src/views/settings/WorkspaceSettings.tsx:54
msgid "Settings | Workspace"
msgstr "Settings | Workspace"
#: src/views/home/components/Header.tsx:100
#: src/views/home/components/Header.tsx:138
@@ -1631,7 +1736,7 @@ msgid "System"
msgstr "System"
#: src/views/members/components/InviteMemberForm.tsx:182
#: src/views/members/index.tsx:188
#: src/views/members/index.tsx:207
msgid "Team Plan"
msgstr "Team Plan"
@@ -1683,6 +1788,10 @@ msgstr "They won't be able to access this workspace."
msgid "This action can't be undone."
msgstr "This action can't be undone."
#: src/views/settings/components/NewApiKeyModal.tsx:129
msgid "This API key will only be shown once. Please save it in a secure location."
msgstr "This API key will only be shown once. Please save it in a secure location."
#: src/views/public/board/index.tsx:151
msgid "This board is private or does not exist"
msgstr "This board is private or does not exist"
@@ -1699,6 +1808,14 @@ msgstr "This will result in the permanent deletion of all data associated with t
msgid "This will result in the permanent deletion of all data associated with your account."
msgstr "This will result in the permanent deletion of all data associated with your account."
#: src/components/NewWorkspaceForm.tsx:284
msgid "This workspace URL has already been taken"
msgstr "This workspace URL has already been taken"
#: src/components/NewWorkspaceForm.tsx:286
msgid "This workspace URL is reserved"
msgstr "This workspace URL is reserved"
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:123
msgid "This workspace username has already been taken"
msgstr "This workspace username has already been taken"
@@ -1720,7 +1837,11 @@ msgstr "Toggle menu"
msgid "Track all card changes with detailed activity history."
msgstr "Track all card changes with detailed activity history."
#: src/views/settings/index.tsx:116
#: src/views/settings/IntegrationsSettings.tsx:73
msgid "Trello"
msgstr "Trello"
#: src/views/settings/IntegrationsSettings.tsx:53
msgid "Trello disconnected"
msgstr "Trello disconnected"
@@ -1752,7 +1873,7 @@ msgstr "Unable to create checklist"
msgid "Unable to create list"
msgstr "Unable to create list"
#: src/components/NewWorkspaceForm.tsx:41
#: src/components/NewWorkspaceForm.tsx:154
msgid "Unable to create workspace"
msgstr "Unable to create workspace"
@@ -1805,16 +1926,16 @@ msgstr "Unable to update checklist item"
msgid "Unable to update comment"
msgstr "Unable to update comment"
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/LabelSelector.tsx:71
msgid "Unable to update labels"
msgstr "Unable to update labels"
#: src/views/board/index.tsx:133
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/ListSelector.tsx:51
msgid "Unable to update list"
msgstr "Unable to update list"
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/MemberSelector.tsx:78
msgid "Unable to update members"
msgstr "Unable to update members"
@@ -1855,14 +1976,15 @@ msgstr "Unlimited comments"
msgid "Unlimited lists"
msgstr "Unlimited lists"
#: src/components/NewWorkspaceForm.tsx:329
#: src/views/settings/components/UpgradeToProConfirmation.tsx:64
msgid "Unlimited members"
msgstr "Unlimited members"
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
#: src/views/settings/components/UpdateDisplayNameForm.tsx:79
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:90
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:82
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:154
msgid "Update"
msgstr "Update"
@@ -1892,15 +2014,19 @@ msgstr "updated the title to <0>{0}</0>"
#~ msgid "updated the title to <0>{toTitle}</0>"
#~ msgstr "updated the title to <0>{toTitle}</0>"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:99
#: src/views/settings/components/UpgradeToProConfirmation.tsx:109
msgid "Upgrade"
msgstr "Upgrade"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:52
#: src/views/settings/index.tsx:213
#: src/views/settings/WorkspaceSettings.tsx:89
msgid "Upgrade to Pro"
msgstr "Upgrade to Pro"
#: src/components/NewWorkspaceForm.tsx:369
msgid "Upgrade to Pro ($29/month)"
msgstr "Upgrade to Pro ($29/month)"
#: src/views/members/components/InviteMemberForm.tsx:224
msgid "Upgrade to Team Plan"
msgstr "Upgrade to Team Plan"
@@ -1910,14 +2036,17 @@ msgstr "Upgrade to Team Plan"
msgid "Urgent"
msgstr "Urgent"
#: src/components/NewWorkspaceForm.tsx:35
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:36
msgid "URL can only contain letters, numbers, and hyphens"
msgstr "URL can only contain letters, numbers, and hyphens"
#: src/components/NewWorkspaceForm.tsx:33
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:34
msgid "URL cannot exceed 24 characters"
msgstr "URL cannot exceed 24 characters"
#: src/components/NewWorkspaceForm.tsx:31
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:32
msgid "URL must be at least 3 characters long"
msgstr "URL must be at least 3 characters long"
@@ -1926,7 +2055,7 @@ msgstr "URL must be at least 3 characters long"
msgid "Use template"
msgstr "Use template"
#: src/views/members/index.tsx:217
#: src/views/members/index.tsx:237
msgid "User"
msgstr "User"
@@ -1938,11 +2067,11 @@ msgstr "User is already a member of this workspace"
msgid "Video"
msgstr "Video"
#: src/views/settings/index.tsx:296
#: src/views/settings/ApiSettings.tsx:25
msgid "View and manage your API keys."
msgstr "View and manage your API keys."
#: src/views/settings/index.tsx:235
#: src/views/settings/BillingSettings.tsx:42
msgid "View and manage your billing and subscription."
msgstr "View and manage your billing and subscription."
@@ -1998,15 +2127,20 @@ msgstr "When Trello launched in 2011, it blew everyone away with its carefully d
msgid "Why make an open source Trello?"
msgstr "Why make an open source Trello?"
#: src/components/SettingsLayout.tsx:39
#: src/views/board/index.tsx:331
msgid "Workspace"
msgstr "Workspace"
#: src/components/NewWorkspaceForm.tsx:143
msgid "Workspace created successfully. You can upgrade later in settings."
msgstr "Workspace created successfully. You can upgrade later in settings."
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:26
msgid "Workspace deleted"
msgstr "Workspace deleted"
#: src/views/settings/index.tsx:199
#: src/views/settings/WorkspaceSettings.tsx:75
msgid "Workspace description"
msgstr "Workspace description"
@@ -2027,8 +2161,8 @@ msgstr "Workspace description updated"
msgid "Workspace members"
msgstr "Workspace members"
#: src/components/NewWorkspaceForm.tsx:81
#: src/views/settings/index.tsx:180
#: src/components/NewWorkspaceForm.tsx:259
#: src/views/settings/WorkspaceSettings.tsx:58
msgid "Workspace name"
msgstr "Workspace name"
@@ -2036,6 +2170,10 @@ msgstr "Workspace name"
msgid "Workspace name cannot exceed 24 characters"
msgstr "Workspace name cannot exceed 24 characters"
#: src/components/NewWorkspaceForm.tsx:27
msgid "Workspace name is required"
msgstr "Workspace name is required"
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:14
msgid "Workspace name must be at least 3 characters long"
msgstr "Workspace name must be at least 3 characters long"
@@ -2048,10 +2186,14 @@ msgstr "Workspace name updated"
msgid "Workspace slug updated"
msgstr "Workspace slug updated"
#: src/views/settings/index.tsx:189
#: src/views/settings/WorkspaceSettings.tsx:66
msgid "Workspace URL"
msgstr "Workspace URL"
#: src/components/NewWorkspaceForm.tsx:273
msgid "workspace-url"
msgstr "workspace-url"
#: src/views/boards/components/TemplateBoards.tsx:30
msgid "Writing"
msgstr "Writing"
@@ -2064,7 +2206,7 @@ msgstr "Yearly"
msgid "Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits."
msgstr "Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits."
#: src/views/settings/index.tsx:328
#: src/views/settings/AccountSettings.tsx:73
msgid "You are about to change your password."
msgstr "You are about to change your password."
@@ -2108,15 +2250,15 @@ msgstr "Your display name has been updated."
msgid "Your password has been changed."
msgstr "Your password has been changed."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Your profile image has been updated."
#: src/views/settings/index.tsx:117
#: src/views/settings/IntegrationsSettings.tsx:54
msgid "Your Trello account has been disconnected."
msgstr "Your Trello account has been disconnected."
#: src/views/settings/index.tsx:278
#: src/views/settings/IntegrationsSettings.tsx:102
msgid "Your Trello account is connected."
msgstr "Your Trello account is connected."

File diff suppressed because one or more lines are too long

View File

@@ -53,6 +53,10 @@ msgstr "1 usuario"
msgid "A powerful, flexible kanban app that helps you organise work, track progress, and deliver results—all in one place."
msgstr "Una aplicación kanban potente y flexible que te ayuda a organizar el trabajo, seguir el progreso y entregar resultados, todo en un solo lugar."
#: src/components/SettingsLayout.tsx:33
msgid "Account"
msgstr "Cuenta"
#: src/views/settings/components/DeleteAccountConfirmation.tsx:27
msgid "Account deleted"
msgstr "Cuenta eliminada"
@@ -91,8 +95,8 @@ msgstr "Añadir descripción... (escribe '/' para abrir comandos o '@' para menc
msgid "Add details..."
msgstr "Añadir detalles..."
#: src/views/card/components/LabelSelector.tsx:110
#: src/views/card/components/LabelSelector.tsx:118
#: src/views/card/components/LabelSelector.tsx:109
#: src/views/card/components/LabelSelector.tsx:114
msgid "Add label"
msgstr "Añadir etiqueta"
@@ -136,6 +140,10 @@ msgstr "añadió la etiqueta <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Añadir un nuevo miembro costará {price} adicionales ({billingType}) por asiento."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Ajusta el recorte cuadrado para que se adapte a tu avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Roles de administrador"
@@ -148,7 +156,7 @@ msgstr "Todos los sistemas operativos"
msgid "Already have an account? <0><1>Sign in</1></0>"
msgstr "¿Ya tienes una cuenta? <0><1>Iniciar sesión</1></0>"
#: src/views/settings/index.tsx:124
#: src/views/settings/IntegrationsSettings.tsx:61
msgid "An error occurred while disconnecting your Trello account."
msgstr "Ocurrió un error al desconectar tu cuenta de Trello."
@@ -156,7 +164,27 @@ msgstr "Ocurrió un error al desconectar tu cuenta de Trello."
msgid "An unexpected error occurred. Please try again later."
msgstr "Ha ocurrido un error inesperado. Por favor, inténtalo de nuevo más tarde."
#: src/views/settings/index.tsx:293
#: src/components/SettingsLayout.tsx:51
msgid "API"
msgstr "API"
#: src/views/settings/components/NewApiKeyModal.tsx:91
msgid "API key created"
msgstr "Clave API creada"
#: src/views/settings/components/NewApiKeyModal.tsx:161
msgid "API key name"
msgstr "Nombre de la clave API"
#: src/views/settings/components/NewApiKeyModal.tsx:25
msgid "API key name cannot exceed 30 characters"
msgstr "El nombre de la clave API no puede exceder los 30 caracteres"
#: src/views/settings/components/NewApiKeyModal.tsx:24
msgid "API key name is required"
msgstr "El nombre de la clave API es obligatorio"
#: src/views/settings/ApiSettings.tsx:22
msgid "API keys"
msgstr "Claves API"
@@ -226,12 +254,13 @@ msgstr "facturado anualmente"
msgid "billed monthly"
msgstr "facturado mensualmente"
#: src/components/SettingsLayout.tsx:44
#: src/views/boards/components/TemplateBoards.tsx:55
#: src/views/settings/index.tsx:232
#: src/views/settings/BillingSettings.tsx:39
msgid "Billing"
msgstr "Facturación"
#: src/views/settings/index.tsx:242
#: src/views/settings/BillingSettings.tsx:49
msgid "Billing portal"
msgstr "Portal de facturación"
@@ -243,9 +272,10 @@ msgstr "Entrada de blog"
msgid "Board"
msgstr "Tablero"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:80
msgid "Board analytics (coming soon)"
msgstr "Análisis de tableros (próximamente)"
#: src/components/NewWorkspaceForm.tsx:346
#: src/views/settings/components/UpgradeToProConfirmation.tsx:81
msgid "Board analytics"
msgstr "Análisis del tablero"
#: src/views/boards/components/NewBoardForm.tsx:21
msgid "Board name cannot exceed 100 characters"
@@ -285,12 +315,12 @@ msgid "Board visibility updated"
msgstr "Visibilidad del tablero actualizada"
#: src/components/SideNavigation.tsx:68
#: src/views/boards/index.tsx:27
#: src/views/boards/index.tsx:32
msgid "Boards"
msgstr "Tableros"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/boards/index.tsx:23
#: src/views/boards/index.tsx:28
msgid "Boards | {0}"
msgstr "Tableros | {0}"
@@ -312,10 +342,11 @@ msgstr "Informe de error"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
#: src/views/settings/components/UpgradeToProConfirmation.tsx:94
#: src/views/settings/components/UpgradeToProConfirmation.tsx:104
msgid "Cancel"
msgstr "Cancelar"
@@ -327,16 +358,16 @@ msgstr "Tarjeta no encontrada"
msgid "Card title"
msgstr "Título de la tarjeta"
#: src/views/settings/AccountSettings.tsx:70
#: src/views/settings/AccountSettings.tsx:80
#: src/views/settings/components/ChangePasswordConfirmation.tsx:109
#: src/views/settings/components/ChangePasswordConfirmation.tsx:178
#: src/views/settings/index.tsx:325
#: src/views/settings/index.tsx:335
msgid "Change Password"
msgstr "Cambiar contraseña"
#: src/views/settings/index.tsx:224
msgid "Change the language of the app."
msgstr "Cambiar el idioma de la aplicación."
#: src/views/settings/AccountSettings.tsx:45
msgid "Change your language preferences."
msgstr "Cambia tus preferencias de idioma."
#: src/views/auth/login/index.tsx:41
#: src/views/auth/signup/index.tsx:67
@@ -356,6 +387,10 @@ msgstr "Borrar filtros"
msgid "Click on the link we've sent to {magicLinkRecipient} to sign in."
msgstr "Haz clic en el enlace que hemos enviado a {magicLinkRecipient} para iniciar sesión."
#: src/views/settings/components/NewApiKeyModal.tsx:136
msgid "Close"
msgstr "Cerrar"
#: src/views/boards/components/TemplateBoards.tsx:22
msgid "Code Review"
msgstr "Revisión de código"
@@ -364,7 +399,9 @@ msgstr "Revisión de código"
msgid "Collaborate seamlessly with your team."
msgstr "Colabora sin problemas con tu equipo."
#: src/components/NewWorkspaceForm.tsx:349
#: src/views/home/components/Features.tsx:66
#: src/views/settings/components/UpgradeToProConfirmation.tsx:84
msgid "Coming soon"
msgstr "Próximamente"
@@ -398,7 +435,7 @@ msgid "Confirm your new password"
msgstr "Confirma tu nueva contraseña"
#: src/views/boards/components/ImportBoardsForm.tsx:157
#: src/views/settings/index.tsx:269
#: src/views/settings/IntegrationsSettings.tsx:93
msgid "Connect Trello"
msgstr "Conectar Trello"
@@ -406,7 +443,7 @@ msgstr "Conectar Trello"
msgid "Connect your favorite tools to streamline your workflow."
msgstr "Conecta tus herramientas favoritas para agilizar tu flujo de trabajo."
#: src/views/settings/index.tsx:256
#: src/views/settings/IntegrationsSettings.tsx:80
msgid "Connect your Trello account to import boards."
msgstr "Conecta tu cuenta de Trello para importar tableros."
@@ -441,6 +478,10 @@ msgstr "Controla quién puede ver y editar tus tableros."
msgid "Create another"
msgstr "Crear otro"
#: src/views/settings/components/NewApiKeyModal.tsx:175
msgid "Create API key"
msgstr "Crear clave API"
#: src/views/boards/components/NewBoardForm.tsx:128
msgid "Create board"
msgstr "Crear tablero"
@@ -465,12 +506,12 @@ msgstr "Crear lista"
msgid "Create new board"
msgstr "Crear nuevo tablero"
#: src/views/settings/components/CreateAPIKeyForm.tsx:54
#: src/views/settings/ApiSettings.tsx:30
msgid "Create new key"
msgstr "Crear nueva clave"
#: src/views/board/components/NewCardForm.tsx:394
#: src/views/card/components/LabelSelector.tsx:98
#: src/views/card/components/LabelSelector.tsx:97
msgid "Create new label"
msgstr "Crear nueva etiqueta"
@@ -478,7 +519,7 @@ msgstr "Crear nueva etiqueta"
msgid "Create new list"
msgstr "Crear nueva lista"
#: src/components/NewWorkspaceForm.tsx:94
#: src/components/NewWorkspaceForm.tsx:384
#: src/components/WorkspaceMenu.tsx:116
msgid "Create workspace"
msgstr "Crear espacio de trabajo"
@@ -491,6 +532,10 @@ msgstr "creó la tarjeta"
msgid "Critical"
msgstr "Crítico"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Recorta tu avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Se requiere la contraseña actual"
@@ -499,6 +544,11 @@ msgstr "Se requiere la contraseña actual"
msgid "Custom domain"
msgstr "Dominio personalizado"
#: src/components/NewWorkspaceForm.tsx:315
msgid "Custom URLs require upgrading to a Pro plan"
msgstr "Las URLs personalizadas requieren actualizar a un plan Pro"
#: src/components/NewWorkspaceForm.tsx:339
#: src/views/settings/components/UpgradeToProConfirmation.tsx:74
msgid "Custom workspace URL"
msgstr "URL personalizada del espacio de trabajo"
@@ -519,9 +569,9 @@ msgstr "Oscuro"
msgid "Delete"
msgstr "Eliminar"
#: src/views/settings/AccountSettings.tsx:52
#: src/views/settings/AccountSettings.tsx:62
#: src/views/settings/components/DeleteAccountConfirmation.tsx:96
#: src/views/settings/index.tsx:343
#: src/views/settings/index.tsx:353
msgid "Delete account"
msgstr "Eliminar cuenta"
@@ -542,8 +592,8 @@ msgid "Delete list"
msgstr "Eliminar lista"
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:106
#: src/views/settings/index.tsx:306
#: src/views/settings/index.tsx:317
#: src/views/settings/WorkspaceSettings.tsx:96
#: src/views/settings/WorkspaceSettings.tsx:107
msgid "Delete workspace"
msgstr "Eliminar espacio de trabajo"
@@ -569,7 +619,7 @@ msgstr "eliminó el elemento <0>{0}</0> de la lista de verificación"
msgid "Design"
msgstr "Diseño"
#: src/views/settings/index.tsx:284
#: src/views/settings/IntegrationsSettings.tsx:108
msgid "Disconnect Trello"
msgstr "Desconectar Trello"
@@ -577,7 +627,7 @@ msgstr "Desconectar Trello"
msgid "Discuss and collaborate on cards."
msgstr "Discute y colabora en las tarjetas."
#: src/views/settings/index.tsx:173
#: src/views/settings/AccountSettings.tsx:35
msgid "Display name"
msgstr "Nombre visible"
@@ -695,7 +745,7 @@ msgstr "Error al eliminar la etiqueta"
msgid "Error deleting workspace"
msgstr "Error al eliminar el espacio de trabajo"
#: src/views/settings/index.tsx:123
#: src/views/settings/IntegrationsSettings.tsx:60
msgid "Error disconnecting Trello"
msgstr "Error al desconectar Trello"
@@ -708,7 +758,7 @@ msgstr "Error al invitar al miembro"
msgid "Error updating display name"
msgstr "Error al actualizar el nombre visible"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Error al actualizar la imagen de perfil"
@@ -729,8 +779,12 @@ msgstr "Error al actualizar la URL del espacio de trabajo"
msgid "Error upgrading subscription"
msgstr "Error al actualizar la suscripción"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/components/NewWorkspaceForm.tsx:142
msgid "Error upgrading to Pro"
msgstr "Error al actualizar a Pro"
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Error al subir la imagen de perfil"
@@ -796,7 +850,7 @@ msgid "Free"
msgstr "Gratis"
#: src/views/members/components/InviteMemberForm.tsx:193
#: src/views/members/index.tsx:189
#: src/views/members/index.tsx:208
msgid "Free Plan"
msgstr "Plan gratuito"
@@ -905,7 +959,7 @@ msgstr "Ideas"
msgid "Ideas to improve this page..."
msgstr "Ideas para mejorar esta página..."
#: src/views/boards/index.tsx:38
#: src/views/boards/index.tsx:43
msgid "Import"
msgstr "Importar"
@@ -943,6 +997,7 @@ msgstr "En progreso"
msgid "Individuals"
msgstr "Individuos"
#: src/components/SettingsLayout.tsx:57
#: src/views/home/components/Features.tsx:114
msgid "Integrations"
msgstr "Integraciones"
@@ -955,7 +1010,7 @@ msgstr "Entrevistando"
msgid "Invalid email address"
msgstr "Dirección de correo electrónico no válida"
#: src/views/members/index.tsx:201
#: src/views/members/index.tsx:221
msgid "Invite"
msgstr "Invitar"
@@ -963,7 +1018,7 @@ msgstr "Invitar"
msgid "Invite another"
msgstr "Invitar a otro"
#: src/views/card/components/MemberSelector.tsx:112
#: src/views/card/components/MemberSelector.tsx:111
#: src/views/members/components/InviteMemberForm.tsx:233
msgid "Invite member"
msgstr "Invitar miembro"
@@ -999,15 +1054,20 @@ msgstr "Etiquetas"
msgid "Labels & Filters"
msgstr "Etiquetas y filtros"
#: src/views/settings/index.tsx:221
#: src/views/settings/AccountSettings.tsx:42
msgid "Language"
msgstr "Idioma"
#: src/components/NewWorkspaceForm.tsx:332
#: src/views/settings/components/UpgradeToProConfirmation.tsx:67
msgid "Launch offer"
msgstr "Oferta de lanzamiento"
#: src/views/home/components/Pricing.tsx:98
#: src/views/members/index.tsx:191
msgid "Launch offer: Get unlimited members with Pro"
msgstr "Oferta de lanzamiento: Obtén miembros ilimitados con Pro"
#: src/views/home/components/Pricing.tsx:99
msgid "Launch offer: unlimited seats for just $29/month with Pro"
msgstr "Oferta de lanzamiento: asientos ilimitados por solo $29/mes con Pro"
@@ -1076,12 +1136,12 @@ msgstr "Prioridad media"
#: src/views/board/components/Filters.tsx:93
#: src/views/board/components/NewCardForm.tsx:357
#: src/views/card/index.tsx:125
#: src/views/members/index.tsx:172
#: src/views/members/index.tsx:178
msgid "Members"
msgstr "Miembros"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/members/index.tsx:168
#: src/views/members/index.tsx:173
msgid "Members | {0}"
msgstr "Miembros | {0}"
@@ -1112,10 +1172,14 @@ msgstr "Nombre"
msgid "Need help?"
msgstr "¿Necesitas ayuda?"
#: src/views/boards/index.tsx:48
#: src/views/boards/index.tsx:53
msgid "New"
msgstr "Nuevo"
#: src/views/settings/components/NewApiKeyModal.tsx:147
msgid "New API key"
msgstr "Nueva clave API"
#: src/views/boards/components/NewBoardForm.tsx:85
msgid "New board"
msgstr "Nuevo tablero"
@@ -1153,7 +1217,7 @@ msgstr "La nueva contraseña debe ser diferente de la contraseña actual"
msgid "New Ticket"
msgstr "Nuevo ticket"
#: src/components/NewWorkspaceForm.tsx:65
#: src/components/NewWorkspaceForm.tsx:240
msgid "New workspace"
msgstr "Nuevo espacio de trabajo"
@@ -1189,11 +1253,11 @@ msgstr "Oferta"
msgid "Onboarding"
msgstr "Incorporación"
#: src/views/settings/index.tsx:346
#: src/views/settings/AccountSettings.tsx:55
msgid "Once you delete your account, there is no going back. This action cannot be undone."
msgstr "Una vez que elimines tu cuenta, no hay vuelta atrás. Esta acción no se puede deshacer."
#: src/views/settings/index.tsx:309
#: src/views/settings/WorkspaceSettings.tsx:99
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
msgstr "Una vez que elimines tu espacio de trabajo, no hay vuelta atrás. Esta acción no se puede deshacer."
@@ -1225,11 +1289,15 @@ msgstr "La contraseña debe tener al menos 8 caracteres"
msgid "Passwords do not match"
msgstr "Las contraseñas no coinciden"
#: src/views/home/components/Pricing.tsx:101
#: src/views/members/index.tsx:134
msgid "Paused"
msgstr "Pausado"
#: src/views/home/components/Pricing.tsx:102
msgid "Payment frequency"
msgstr "Frecuencia de pago"
#: src/views/members/index.tsx:129
#: src/views/members/index.tsx:134
msgid "Pending"
msgstr "Pendiente"
@@ -1262,13 +1330,13 @@ msgstr "Por favor, introduce un nombre válido"
msgid "Please enter a valid password"
msgstr "Por favor, introduce una contraseña válida"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Por favor selecciona un archivo para subir."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:42
#: src/components/NewWorkspaceForm.tsx:155
#: src/views/board/components/NewCardForm.tsx:174
#: src/views/board/components/NewListForm.tsx:78
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
@@ -1283,9 +1351,9 @@ msgstr "Por favor selecciona un archivo para subir."
#: src/views/card/components/DeleteCardConfirmation.tsx:52
#: src/views/card/components/DeleteChecklistConfirmation.tsx:37
#: src/views/card/components/DeleteCommentConfirmation.tsx:45
#: src/views/card/components/LabelSelector.tsx:73
#: src/views/card/components/ListSelector.tsx:53
#: src/views/card/components/MemberSelector.tsx:80
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/NewChecklistForm.tsx:70
#: src/views/card/components/NewChecklistItemForm.tsx:89
#: src/views/card/components/NewCommentForm.tsx:31
@@ -1293,8 +1361,8 @@ msgstr "Por favor selecciona un archivo para subir."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1312,7 +1380,7 @@ msgid "Pricing"
msgstr "Precios"
#: src/views/home/components/Pricing.tsx:56
#: src/views/settings/components/UpgradeToProConfirmation.tsx:86
#: src/views/settings/components/UpgradeToProConfirmation.tsx:91
msgid "Priority email support"
msgstr "Soporte prioritario por email"
@@ -1324,7 +1392,7 @@ msgstr "Política de privacidad"
msgid "Private"
msgstr "Privado"
#: src/views/members/index.tsx:186
#: src/views/members/index.tsx:205
msgid "Pro Plan"
msgstr "Plan Pro"
@@ -1332,11 +1400,11 @@ msgstr "Plan Pro"
msgid "Pro Plan ∞"
msgstr "Plan Pro ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Imagen de perfil actualizada"
#: src/views/settings/index.tsx:168
#: src/views/settings/AccountSettings.tsx:29
msgid "Profile picture"
msgstr "Foto de perfil"
@@ -1372,7 +1440,7 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Eliminar"
#: src/views/members/index.tsx:143
#: src/views/members/index.tsx:148
msgid "Remove member"
msgstr "Eliminar miembro"
@@ -1419,16 +1487,12 @@ msgstr "Recursos"
msgid "Review"
msgstr "Revisión"
#: src/views/settings/components/CreateAPIKeyForm.tsx:49
msgid "Revoke"
msgstr "Revocar"
#: src/views/home/components/Footer.tsx:36
#: src/views/home/components/Header.tsx:13
msgid "Roadmap"
msgstr "Hoja de ruta"
#: src/views/members/index.tsx:223
#: src/views/members/index.tsx:243
msgid "Role"
msgstr "Rol"
@@ -1437,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Ejecuta en tu propia infraestructura"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Guardar"
@@ -1476,15 +1541,30 @@ msgstr "Enviar comentarios"
msgid "Senior"
msgstr "Senior"
#: src/components/SettingsLayout.tsx:82
#: src/components/SideNavigation.tsx:78
#: src/views/settings/index.tsx:162
msgid "Settings"
msgstr "Configuración"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/settings/index.tsx:158
msgid "Settings | {0}"
msgstr "Configuración | {0}"
#: src/views/settings/AccountSettings.tsx:25
msgid "Settings | Account"
msgstr "Configuración | Cuenta"
#: src/views/settings/ApiSettings.tsx:18
msgid "Settings | API"
msgstr "Configuración | API"
#: src/views/settings/BillingSettings.tsx:35
msgid "Settings | Billing"
msgstr "Configuración | Facturación"
#: src/views/settings/IntegrationsSettings.tsx:69
msgid "Settings | Integrations"
msgstr "Configuración | Integraciones"
#: src/views/settings/WorkspaceSettings.tsx:54
msgid "Settings | Workspace"
msgstr "Configuración | Espacio de trabajo"
#: src/views/home/components/Header.tsx:100
#: src/views/home/components/Header.tsx:138
@@ -1550,7 +1630,7 @@ msgid "System"
msgstr "Sistema"
#: src/views/members/components/InviteMemberForm.tsx:182
#: src/views/members/index.tsx:188
#: src/views/members/index.tsx:207
msgid "Team Plan"
msgstr "Plan de Equipo"
@@ -1602,6 +1682,10 @@ msgstr "No podrán acceder a este espacio de trabajo."
msgid "This action can't be undone."
msgstr "Esta acción no se puede deshacer."
#: src/views/settings/components/NewApiKeyModal.tsx:129
msgid "This API key will only be shown once. Please save it in a secure location."
msgstr "Esta clave API solo se mostrará una vez. Por favor, guárdala en un lugar seguro."
#: src/views/public/board/index.tsx:151
msgid "This board is private or does not exist"
msgstr "Este tablero es privado o no existe"
@@ -1618,6 +1702,14 @@ msgstr "Esto resultará en la eliminación permanente de todos los datos asociad
msgid "This will result in the permanent deletion of all data associated with your account."
msgstr "Esto resultará en la eliminación permanente de todos los datos asociados con tu cuenta."
#: src/components/NewWorkspaceForm.tsx:284
msgid "This workspace URL has already been taken"
msgstr "Esta URL de espacio de trabajo ya ha sido tomada"
#: src/components/NewWorkspaceForm.tsx:286
msgid "This workspace URL is reserved"
msgstr "Esta URL de espacio de trabajo está reservada"
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:123
msgid "This workspace username has already been taken"
msgstr "Este nombre de usuario del espacio de trabajo ya ha sido tomado"
@@ -1635,7 +1727,11 @@ msgstr "Alternar menú"
msgid "Track all card changes with detailed activity history."
msgstr "Rastrea todos los cambios en las tarjetas con un historial de actividad detallado."
#: src/views/settings/index.tsx:116
#: src/views/settings/IntegrationsSettings.tsx:73
msgid "Trello"
msgstr "Trello"
#: src/views/settings/IntegrationsSettings.tsx:53
msgid "Trello disconnected"
msgstr "Trello desconectado"
@@ -1667,7 +1763,7 @@ msgstr "No se puede crear la lista de verificación"
msgid "Unable to create list"
msgstr "No se puede crear la lista"
#: src/components/NewWorkspaceForm.tsx:41
#: src/components/NewWorkspaceForm.tsx:154
msgid "Unable to create workspace"
msgstr "No se puede crear el espacio de trabajo"
@@ -1720,16 +1816,16 @@ msgstr "No se puede actualizar el elemento de la lista de verificación"
msgid "Unable to update comment"
msgstr "No se puede actualizar el comentario"
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/LabelSelector.tsx:71
msgid "Unable to update labels"
msgstr "No se pueden actualizar las etiquetas"
#: src/views/board/index.tsx:133
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/ListSelector.tsx:51
msgid "Unable to update list"
msgstr "No se puede actualizar la lista"
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/MemberSelector.tsx:78
msgid "Unable to update members"
msgstr "No se pueden actualizar los miembros"
@@ -1766,14 +1862,15 @@ msgstr "Comentarios ilimitados"
msgid "Unlimited lists"
msgstr "Listas ilimitadas"
#: src/components/NewWorkspaceForm.tsx:329
#: src/views/settings/components/UpgradeToProConfirmation.tsx:64
msgid "Unlimited members"
msgstr "Miembros ilimitados"
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
#: src/views/settings/components/UpdateDisplayNameForm.tsx:79
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:90
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:82
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:154
msgid "Update"
msgstr "Actualizar"
@@ -1799,15 +1896,19 @@ msgstr "actualizó el título"
msgid "updated the title to <0>{0}</0>"
msgstr "actualizó el título a <0>{0}</0>"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:99
#: src/views/settings/components/UpgradeToProConfirmation.tsx:109
msgid "Upgrade"
msgstr "Actualizar"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:52
#: src/views/settings/index.tsx:213
#: src/views/settings/WorkspaceSettings.tsx:89
msgid "Upgrade to Pro"
msgstr "Actualizar a Pro"
#: src/components/NewWorkspaceForm.tsx:369
msgid "Upgrade to Pro ($29/month)"
msgstr "Actualizar a Pro ($29/mes)"
#: src/views/members/components/InviteMemberForm.tsx:224
msgid "Upgrade to Team Plan"
msgstr "Actualizar al Plan de Equipo"
@@ -1817,14 +1918,17 @@ msgstr "Actualizar al Plan de Equipo"
msgid "Urgent"
msgstr "Urgente"
#: src/components/NewWorkspaceForm.tsx:35
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:36
msgid "URL can only contain letters, numbers, and hyphens"
msgstr "La URL solo puede contener letras, números y guiones"
#: src/components/NewWorkspaceForm.tsx:33
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:34
msgid "URL cannot exceed 24 characters"
msgstr "La URL no puede exceder los 24 caracteres"
#: src/components/NewWorkspaceForm.tsx:31
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:32
msgid "URL must be at least 3 characters long"
msgstr "La URL debe tener al menos 3 caracteres"
@@ -1833,7 +1937,7 @@ msgstr "La URL debe tener al menos 3 caracteres"
msgid "Use template"
msgstr "Usar plantilla"
#: src/views/members/index.tsx:217
#: src/views/members/index.tsx:237
msgid "User"
msgstr "Usuario"
@@ -1845,11 +1949,11 @@ msgstr "El usuario ya es miembro de este espacio de trabajo"
msgid "Video"
msgstr "Video"
#: src/views/settings/index.tsx:296
#: src/views/settings/ApiSettings.tsx:25
msgid "View and manage your API keys."
msgstr "Ver y gestionar tus claves API."
#: src/views/settings/index.tsx:235
#: src/views/settings/BillingSettings.tsx:42
msgid "View and manage your billing and subscription."
msgstr "Ver y gestionar tu facturación y suscripción."
@@ -1901,15 +2005,20 @@ msgstr "Cuando Trello se lanzó en 2011, impresionó a todos con su simplicidad
msgid "Why make an open source Trello?"
msgstr "¿Por qué crear un Trello de código abierto?"
#: src/components/SettingsLayout.tsx:39
#: src/views/board/index.tsx:331
msgid "Workspace"
msgstr "Espacio de trabajo"
#: src/components/NewWorkspaceForm.tsx:143
msgid "Workspace created successfully. You can upgrade later in settings."
msgstr "Espacio de trabajo creado con éxito. Puedes actualizar más tarde en configuración."
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:26
msgid "Workspace deleted"
msgstr "Espacio de trabajo eliminado"
#: src/views/settings/index.tsx:199
#: src/views/settings/WorkspaceSettings.tsx:75
msgid "Workspace description"
msgstr "Descripción del espacio de trabajo"
@@ -1930,8 +2039,8 @@ msgstr "Descripción del espacio de trabajo actualizada"
msgid "Workspace members"
msgstr "Miembros del espacio de trabajo"
#: src/components/NewWorkspaceForm.tsx:81
#: src/views/settings/index.tsx:180
#: src/components/NewWorkspaceForm.tsx:259
#: src/views/settings/WorkspaceSettings.tsx:58
msgid "Workspace name"
msgstr "Nombre del espacio de trabajo"
@@ -1939,6 +2048,10 @@ msgstr "Nombre del espacio de trabajo"
msgid "Workspace name cannot exceed 24 characters"
msgstr "El nombre del espacio de trabajo no puede exceder los 24 caracteres"
#: src/components/NewWorkspaceForm.tsx:27
msgid "Workspace name is required"
msgstr "El nombre del espacio de trabajo es obligatorio"
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:14
msgid "Workspace name must be at least 3 characters long"
msgstr "El nombre del espacio de trabajo debe tener al menos 3 caracteres"
@@ -1951,10 +2064,14 @@ msgstr "Nombre del espacio de trabajo actualizado"
msgid "Workspace slug updated"
msgstr "Slug del espacio de trabajo actualizado"
#: src/views/settings/index.tsx:189
#: src/views/settings/WorkspaceSettings.tsx:66
msgid "Workspace URL"
msgstr "URL del espacio de trabajo"
#: src/components/NewWorkspaceForm.tsx:273
msgid "workspace-url"
msgstr "workspace-url"
#: src/views/boards/components/TemplateBoards.tsx:30
msgid "Writing"
msgstr "Escritura"
@@ -1967,7 +2084,7 @@ msgstr "Anual"
msgid "Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits."
msgstr "Sí, ofrecemos un plan gratuito para siempre para uso individual. Sin restricciones, sin muros de pago, sin límites."
#: src/views/settings/index.tsx:328
#: src/views/settings/AccountSettings.tsx:73
msgid "You are about to change your password."
msgstr "Estás a punto de cambiar tu contraseña."
@@ -2011,15 +2128,15 @@ msgstr "Tu nombre de visualización ha sido actualizado."
msgid "Your password has been changed."
msgstr "Tu contraseña ha sido cambiada."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Tu imagen de perfil ha sido actualizada."
#: src/views/settings/index.tsx:117
#: src/views/settings/IntegrationsSettings.tsx:54
msgid "Your Trello account has been disconnected."
msgstr "Tu cuenta de Trello ha sido desconectada."
#: src/views/settings/index.tsx:278
#: src/views/settings/IntegrationsSettings.tsx:102
msgid "Your Trello account is connected."
msgstr "Tu cuenta de Trello está conectada."

File diff suppressed because one or more lines are too long

View File

@@ -53,6 +53,10 @@ msgstr "1 utilisateur"
msgid "A powerful, flexible kanban app that helps you organise work, track progress, and deliver results—all in one place."
msgstr "Une application kanban puissante et flexible qui vous aide à organiser le travail, suivre les progrès et livrer des résultats—le tout en un seul endroit."
#: src/components/SettingsLayout.tsx:33
msgid "Account"
msgstr "Compte"
#: src/views/settings/components/DeleteAccountConfirmation.tsx:27
msgid "Account deleted"
msgstr "Compte supprimé"
@@ -91,8 +95,8 @@ msgstr "Ajouter une description... (tapez '/' pour ouvrir les commandes ou '@' p
msgid "Add details..."
msgstr "Ajouter des détails..."
#: src/views/card/components/LabelSelector.tsx:110
#: src/views/card/components/LabelSelector.tsx:118
#: src/views/card/components/LabelSelector.tsx:109
#: src/views/card/components/LabelSelector.tsx:114
msgid "Add label"
msgstr "Ajouter une étiquette"
@@ -136,6 +140,10 @@ msgstr "a ajouté l'étiquette <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'ajout d'un nouveau membre coûtera {price} supplémentaires ({billingType}) par siège."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Ajustez le recadrage carré pour adapter votre avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Rôles d'administrateur"
@@ -148,7 +156,7 @@ msgstr "Tous les systèmes opérationnels"
msgid "Already have an account? <0><1>Sign in</1></0>"
msgstr "Vous avez déjà un compte ? <0><1>Connectez-vous</1></0>"
#: src/views/settings/index.tsx:124
#: src/views/settings/IntegrationsSettings.tsx:61
msgid "An error occurred while disconnecting your Trello account."
msgstr "Une erreur s'est produite lors de la déconnexion de votre compte Trello."
@@ -156,7 +164,27 @@ msgstr "Une erreur s'est produite lors de la déconnexion de votre compte Trello
msgid "An unexpected error occurred. Please try again later."
msgstr "Une erreur inattendue s'est produite. Veuillez réessayer plus tard."
#: src/views/settings/index.tsx:293
#: src/components/SettingsLayout.tsx:51
msgid "API"
msgstr "API"
#: src/views/settings/components/NewApiKeyModal.tsx:91
msgid "API key created"
msgstr "Clé API créée"
#: src/views/settings/components/NewApiKeyModal.tsx:161
msgid "API key name"
msgstr "Nom de la clé API"
#: src/views/settings/components/NewApiKeyModal.tsx:25
msgid "API key name cannot exceed 30 characters"
msgstr "Le nom de la clé API ne peut pas dépasser 30 caractères"
#: src/views/settings/components/NewApiKeyModal.tsx:24
msgid "API key name is required"
msgstr "Le nom de la clé API est requis"
#: src/views/settings/ApiSettings.tsx:22
msgid "API keys"
msgstr "Clés API"
@@ -226,12 +254,13 @@ msgstr "facturation annuelle"
msgid "billed monthly"
msgstr "facturation mensuelle"
#: src/components/SettingsLayout.tsx:44
#: src/views/boards/components/TemplateBoards.tsx:55
#: src/views/settings/index.tsx:232
#: src/views/settings/BillingSettings.tsx:39
msgid "Billing"
msgstr "Facturation"
#: src/views/settings/index.tsx:242
#: src/views/settings/BillingSettings.tsx:49
msgid "Billing portal"
msgstr "Portail de facturation"
@@ -243,9 +272,10 @@ msgstr "Article de blog"
msgid "Board"
msgstr "Tableau"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:80
msgid "Board analytics (coming soon)"
msgstr "Analyses de tableau (bientôt disponible)"
#: src/components/NewWorkspaceForm.tsx:346
#: src/views/settings/components/UpgradeToProConfirmation.tsx:81
msgid "Board analytics"
msgstr "Analyses du tableau"
#: src/views/boards/components/NewBoardForm.tsx:21
msgid "Board name cannot exceed 100 characters"
@@ -285,12 +315,12 @@ msgid "Board visibility updated"
msgstr "Visibilité du tableau mise à jour"
#: src/components/SideNavigation.tsx:68
#: src/views/boards/index.tsx:27
#: src/views/boards/index.tsx:32
msgid "Boards"
msgstr "Tableaux"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/boards/index.tsx:23
#: src/views/boards/index.tsx:28
msgid "Boards | {0}"
msgstr "Tableaux | {0}"
@@ -312,10 +342,11 @@ msgstr "Rapport de bug"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
#: src/views/settings/components/UpgradeToProConfirmation.tsx:94
#: src/views/settings/components/UpgradeToProConfirmation.tsx:104
msgid "Cancel"
msgstr "Annuler"
@@ -327,16 +358,16 @@ msgstr "Carte introuvable"
msgid "Card title"
msgstr "Titre de la carte"
#: src/views/settings/AccountSettings.tsx:70
#: src/views/settings/AccountSettings.tsx:80
#: src/views/settings/components/ChangePasswordConfirmation.tsx:109
#: src/views/settings/components/ChangePasswordConfirmation.tsx:178
#: src/views/settings/index.tsx:325
#: src/views/settings/index.tsx:335
msgid "Change Password"
msgstr "Modifier le mot de passe"
#: src/views/settings/index.tsx:224
msgid "Change the language of the app."
msgstr "Changer la langue de l'application."
#: src/views/settings/AccountSettings.tsx:45
msgid "Change your language preferences."
msgstr "Modifiez vos préférences linguistiques."
#: src/views/auth/login/index.tsx:41
#: src/views/auth/signup/index.tsx:67
@@ -356,6 +387,10 @@ msgstr "Effacer les filtres"
msgid "Click on the link we've sent to {magicLinkRecipient} to sign in."
msgstr "Cliquez sur le lien que nous avons envoyé à {magicLinkRecipient} pour vous connecter."
#: src/views/settings/components/NewApiKeyModal.tsx:136
msgid "Close"
msgstr "Fermer"
#: src/views/boards/components/TemplateBoards.tsx:22
msgid "Code Review"
msgstr "Revue de code"
@@ -364,7 +399,9 @@ msgstr "Revue de code"
msgid "Collaborate seamlessly with your team."
msgstr "Collaborez en toute fluidité avec votre équipe."
#: src/components/NewWorkspaceForm.tsx:349
#: src/views/home/components/Features.tsx:66
#: src/views/settings/components/UpgradeToProConfirmation.tsx:84
msgid "Coming soon"
msgstr "Bientôt disponible"
@@ -398,7 +435,7 @@ msgid "Confirm your new password"
msgstr "Confirmez votre nouveau mot de passe"
#: src/views/boards/components/ImportBoardsForm.tsx:157
#: src/views/settings/index.tsx:269
#: src/views/settings/IntegrationsSettings.tsx:93
msgid "Connect Trello"
msgstr "Connecter Trello"
@@ -406,7 +443,7 @@ msgstr "Connecter Trello"
msgid "Connect your favorite tools to streamline your workflow."
msgstr "Connectez vos outils favoris pour simplifier votre flux de travail."
#: src/views/settings/index.tsx:256
#: src/views/settings/IntegrationsSettings.tsx:80
msgid "Connect your Trello account to import boards."
msgstr "Connectez votre compte Trello pour importer des tableaux."
@@ -441,6 +478,10 @@ msgstr "Contrôlez qui peut voir et modifier vos tableaux."
msgid "Create another"
msgstr "Créer un autre"
#: src/views/settings/components/NewApiKeyModal.tsx:175
msgid "Create API key"
msgstr "Créer une clé API"
#: src/views/boards/components/NewBoardForm.tsx:128
msgid "Create board"
msgstr "Créer un tableau"
@@ -465,12 +506,12 @@ msgstr "Créer une liste"
msgid "Create new board"
msgstr "Créer un nouveau tableau"
#: src/views/settings/components/CreateAPIKeyForm.tsx:54
#: src/views/settings/ApiSettings.tsx:30
msgid "Create new key"
msgstr "Créer une nouvelle clé"
#: src/views/board/components/NewCardForm.tsx:394
#: src/views/card/components/LabelSelector.tsx:98
#: src/views/card/components/LabelSelector.tsx:97
msgid "Create new label"
msgstr "Créer une nouvelle étiquette"
@@ -478,7 +519,7 @@ msgstr "Créer une nouvelle étiquette"
msgid "Create new list"
msgstr "Créer une nouvelle liste"
#: src/components/NewWorkspaceForm.tsx:94
#: src/components/NewWorkspaceForm.tsx:384
#: src/components/WorkspaceMenu.tsx:116
msgid "Create workspace"
msgstr "Créer un espace de travail"
@@ -491,6 +532,10 @@ msgstr "a créé la carte"
msgid "Critical"
msgstr "Critique"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Recadrez votre avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Le mot de passe actuel est requis"
@@ -499,6 +544,11 @@ msgstr "Le mot de passe actuel est requis"
msgid "Custom domain"
msgstr "Domaine personnalisé"
#: src/components/NewWorkspaceForm.tsx:315
msgid "Custom URLs require upgrading to a Pro plan"
msgstr "Les URL personnalisées nécessitent une mise à niveau vers un forfait Pro"
#: src/components/NewWorkspaceForm.tsx:339
#: src/views/settings/components/UpgradeToProConfirmation.tsx:74
msgid "Custom workspace URL"
msgstr "URL d'espace de travail personnalisée"
@@ -519,9 +569,9 @@ msgstr "Sombre"
msgid "Delete"
msgstr "Supprimer"
#: src/views/settings/AccountSettings.tsx:52
#: src/views/settings/AccountSettings.tsx:62
#: src/views/settings/components/DeleteAccountConfirmation.tsx:96
#: src/views/settings/index.tsx:343
#: src/views/settings/index.tsx:353
msgid "Delete account"
msgstr "Supprimer le compte"
@@ -542,8 +592,8 @@ msgid "Delete list"
msgstr "Supprimer la liste"
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:106
#: src/views/settings/index.tsx:306
#: src/views/settings/index.tsx:317
#: src/views/settings/WorkspaceSettings.tsx:96
#: src/views/settings/WorkspaceSettings.tsx:107
msgid "Delete workspace"
msgstr "Supprimer l'espace de travail"
@@ -569,7 +619,7 @@ msgstr "a supprimé l'élément <0>{0}</0> de la checklist"
msgid "Design"
msgstr "Design"
#: src/views/settings/index.tsx:284
#: src/views/settings/IntegrationsSettings.tsx:108
msgid "Disconnect Trello"
msgstr "Déconnecter Trello"
@@ -577,7 +627,7 @@ msgstr "Déconnecter Trello"
msgid "Discuss and collaborate on cards."
msgstr "Discutez et collaborez sur les cartes."
#: src/views/settings/index.tsx:173
#: src/views/settings/AccountSettings.tsx:35
msgid "Display name"
msgstr "Nom d'affichage"
@@ -695,7 +745,7 @@ msgstr "Erreur lors de la suppression de l'étiquette"
msgid "Error deleting workspace"
msgstr "Erreur lors de la suppression de l'espace de travail"
#: src/views/settings/index.tsx:123
#: src/views/settings/IntegrationsSettings.tsx:60
msgid "Error disconnecting Trello"
msgstr "Erreur lors de la déconnexion de Trello"
@@ -708,7 +758,7 @@ msgstr "Erreur lors de l'invitation du membre"
msgid "Error updating display name"
msgstr "Erreur lors de la mise à jour du nom d'affichage"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Erreur lors de la mise à jour de l'image de profil"
@@ -729,8 +779,12 @@ msgstr "Erreur lors de la mise à jour de l'URL de l'espace de travail"
msgid "Error upgrading subscription"
msgstr "Erreur lors de la mise à niveau de l'abonnement"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/components/NewWorkspaceForm.tsx:142
msgid "Error upgrading to Pro"
msgstr "Erreur lors de la mise à niveau vers Pro"
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Erreur lors du téléchargement de l'image de profil"
@@ -796,7 +850,7 @@ msgid "Free"
msgstr "Gratuit"
#: src/views/members/components/InviteMemberForm.tsx:193
#: src/views/members/index.tsx:189
#: src/views/members/index.tsx:208
msgid "Free Plan"
msgstr "Forfait gratuit"
@@ -905,7 +959,7 @@ msgstr "Idées"
msgid "Ideas to improve this page..."
msgstr "Idées pour améliorer cette page..."
#: src/views/boards/index.tsx:38
#: src/views/boards/index.tsx:43
msgid "Import"
msgstr "Importer"
@@ -943,6 +997,7 @@ msgstr "En cours"
msgid "Individuals"
msgstr "Particuliers"
#: src/components/SettingsLayout.tsx:57
#: src/views/home/components/Features.tsx:114
msgid "Integrations"
msgstr "Intégrations"
@@ -955,7 +1010,7 @@ msgstr "Entretien"
msgid "Invalid email address"
msgstr "Adresse e-mail invalide"
#: src/views/members/index.tsx:201
#: src/views/members/index.tsx:221
msgid "Invite"
msgstr "Inviter"
@@ -963,7 +1018,7 @@ msgstr "Inviter"
msgid "Invite another"
msgstr "Inviter un autre"
#: src/views/card/components/MemberSelector.tsx:112
#: src/views/card/components/MemberSelector.tsx:111
#: src/views/members/components/InviteMemberForm.tsx:233
msgid "Invite member"
msgstr "Inviter un membre"
@@ -999,15 +1054,20 @@ msgstr "Étiquettes"
msgid "Labels & Filters"
msgstr "Étiquettes & filtres"
#: src/views/settings/index.tsx:221
#: src/views/settings/AccountSettings.tsx:42
msgid "Language"
msgstr "Langue"
#: src/components/NewWorkspaceForm.tsx:332
#: src/views/settings/components/UpgradeToProConfirmation.tsx:67
msgid "Launch offer"
msgstr "Offre de lancement"
#: src/views/home/components/Pricing.tsx:98
#: src/views/members/index.tsx:191
msgid "Launch offer: Get unlimited members with Pro"
msgstr "Offre de lancement : obtenez des membres illimités avec Pro"
#: src/views/home/components/Pricing.tsx:99
msgid "Launch offer: unlimited seats for just $29/month with Pro"
msgstr "Offre de lancement : sièges illimités pour seulement 29 $/mois avec Pro"
@@ -1076,12 +1136,12 @@ msgstr "Priorité moyenne"
#: src/views/board/components/Filters.tsx:93
#: src/views/board/components/NewCardForm.tsx:357
#: src/views/card/index.tsx:125
#: src/views/members/index.tsx:172
#: src/views/members/index.tsx:178
msgid "Members"
msgstr "Membres"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/members/index.tsx:168
#: src/views/members/index.tsx:173
msgid "Members | {0}"
msgstr "Membres | {0}"
@@ -1112,10 +1172,14 @@ msgstr "Nom"
msgid "Need help?"
msgstr "Besoin d'aide ?"
#: src/views/boards/index.tsx:48
#: src/views/boards/index.tsx:53
msgid "New"
msgstr "Nouveau"
#: src/views/settings/components/NewApiKeyModal.tsx:147
msgid "New API key"
msgstr "Nouvelle clé API"
#: src/views/boards/components/NewBoardForm.tsx:85
msgid "New board"
msgstr "Nouveau tableau"
@@ -1153,7 +1217,7 @@ msgstr "Le nouveau mot de passe doit être différent du mot de passe actuel"
msgid "New Ticket"
msgstr "Nouveau ticket"
#: src/components/NewWorkspaceForm.tsx:65
#: src/components/NewWorkspaceForm.tsx:240
msgid "New workspace"
msgstr "Nouvel espace de travail"
@@ -1189,11 +1253,11 @@ msgstr "Offre"
msgid "Onboarding"
msgstr "Intégration"
#: src/views/settings/index.tsx:346
#: src/views/settings/AccountSettings.tsx:55
msgid "Once you delete your account, there is no going back. This action cannot be undone."
msgstr "Une fois que vous supprimez votre compte, il n'y a pas de retour possible. Cette action ne peut pas être annulée."
#: src/views/settings/index.tsx:309
#: src/views/settings/WorkspaceSettings.tsx:99
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
msgstr "Une fois que vous supprimez votre espace de travail, il n'y a pas de retour possible. Cette action ne peut pas être annulée."
@@ -1225,11 +1289,15 @@ msgstr "Le mot de passe doit comporter au moins 8 caractères"
msgid "Passwords do not match"
msgstr "Les mots de passe ne correspondent pas"
#: src/views/home/components/Pricing.tsx:101
#: src/views/members/index.tsx:134
msgid "Paused"
msgstr "En pause"
#: src/views/home/components/Pricing.tsx:102
msgid "Payment frequency"
msgstr "Fréquence de paiement"
#: src/views/members/index.tsx:129
#: src/views/members/index.tsx:134
msgid "Pending"
msgstr "En attente"
@@ -1262,13 +1330,13 @@ msgstr "Veuillez saisir un nom valide"
msgid "Please enter a valid password"
msgstr "Veuillez saisir un mot de passe valide"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Veuillez sélectionner un fichier à télécharger."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:42
#: src/components/NewWorkspaceForm.tsx:155
#: src/views/board/components/NewCardForm.tsx:174
#: src/views/board/components/NewListForm.tsx:78
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
@@ -1283,9 +1351,9 @@ msgstr "Veuillez sélectionner un fichier à télécharger."
#: src/views/card/components/DeleteCardConfirmation.tsx:52
#: src/views/card/components/DeleteChecklistConfirmation.tsx:37
#: src/views/card/components/DeleteCommentConfirmation.tsx:45
#: src/views/card/components/LabelSelector.tsx:73
#: src/views/card/components/ListSelector.tsx:53
#: src/views/card/components/MemberSelector.tsx:80
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/NewChecklistForm.tsx:70
#: src/views/card/components/NewChecklistItemForm.tsx:89
#: src/views/card/components/NewCommentForm.tsx:31
@@ -1293,8 +1361,8 @@ msgstr "Veuillez sélectionner un fichier à télécharger."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1312,7 +1380,7 @@ msgid "Pricing"
msgstr "Tarifs"
#: src/views/home/components/Pricing.tsx:56
#: src/views/settings/components/UpgradeToProConfirmation.tsx:86
#: src/views/settings/components/UpgradeToProConfirmation.tsx:91
msgid "Priority email support"
msgstr "Support par e-mail prioritaire"
@@ -1324,7 +1392,7 @@ msgstr "Politique de confidentialité"
msgid "Private"
msgstr "Privé"
#: src/views/members/index.tsx:186
#: src/views/members/index.tsx:205
msgid "Pro Plan"
msgstr "Plan Pro"
@@ -1332,11 +1400,11 @@ msgstr "Plan Pro"
msgid "Pro Plan ∞"
msgstr "Plan Pro ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Image de profil mise à jour"
#: src/views/settings/index.tsx:168
#: src/views/settings/AccountSettings.tsx:29
msgid "Profile picture"
msgstr "Photo de profil"
@@ -1372,7 +1440,7 @@ msgstr "À distance"
msgid "Remove"
msgstr "Supprimer"
#: src/views/members/index.tsx:143
#: src/views/members/index.tsx:148
msgid "Remove member"
msgstr "Supprimer le membre"
@@ -1419,16 +1487,12 @@ msgstr "Ressources"
msgid "Review"
msgstr "Révision"
#: src/views/settings/components/CreateAPIKeyForm.tsx:49
msgid "Revoke"
msgstr "Révoquer"
#: src/views/home/components/Footer.tsx:36
#: src/views/home/components/Header.tsx:13
msgid "Roadmap"
msgstr "Feuille de route"
#: src/views/members/index.tsx:223
#: src/views/members/index.tsx:243
msgid "Role"
msgstr "Rôle"
@@ -1437,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Exécutez sur votre propre infrastructure"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Enregistrer"
@@ -1476,15 +1541,30 @@ msgstr "Envoyer des commentaires"
msgid "Senior"
msgstr "Senior"
#: src/components/SettingsLayout.tsx:82
#: src/components/SideNavigation.tsx:78
#: src/views/settings/index.tsx:162
msgid "Settings"
msgstr "Paramètres"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/settings/index.tsx:158
msgid "Settings | {0}"
msgstr "Paramètres | {0}"
#: src/views/settings/AccountSettings.tsx:25
msgid "Settings | Account"
msgstr "Paramètres | Compte"
#: src/views/settings/ApiSettings.tsx:18
msgid "Settings | API"
msgstr "Paramètres | API"
#: src/views/settings/BillingSettings.tsx:35
msgid "Settings | Billing"
msgstr "Paramètres | Facturation"
#: src/views/settings/IntegrationsSettings.tsx:69
msgid "Settings | Integrations"
msgstr "Paramètres | Intégrations"
#: src/views/settings/WorkspaceSettings.tsx:54
msgid "Settings | Workspace"
msgstr "Paramètres | Espace de travail"
#: src/views/home/components/Header.tsx:100
#: src/views/home/components/Header.tsx:138
@@ -1550,7 +1630,7 @@ msgid "System"
msgstr "Système"
#: src/views/members/components/InviteMemberForm.tsx:182
#: src/views/members/index.tsx:188
#: src/views/members/index.tsx:207
msgid "Team Plan"
msgstr "Forfait d'équipe"
@@ -1602,6 +1682,10 @@ msgstr "Ils ne pourront plus accéder à cet espace de travail."
msgid "This action can't be undone."
msgstr "Cette action ne peut pas être annulée."
#: src/views/settings/components/NewApiKeyModal.tsx:129
msgid "This API key will only be shown once. Please save it in a secure location."
msgstr "Cette clé API ne sera affichée qu'une seule fois. Veuillez la sauvegarder dans un emplacement sécurisé."
#: src/views/public/board/index.tsx:151
msgid "This board is private or does not exist"
msgstr "Ce tableau est privé ou n'existe pas"
@@ -1618,6 +1702,14 @@ msgstr "Cela entraînera la suppression définitive de toutes les données assoc
msgid "This will result in the permanent deletion of all data associated with your account."
msgstr "Cela entraînera la suppression définitive de toutes les données associées à votre compte."
#: src/components/NewWorkspaceForm.tsx:284
msgid "This workspace URL has already been taken"
msgstr "Cette URL d'espace de travail est déjà prise"
#: src/components/NewWorkspaceForm.tsx:286
msgid "This workspace URL is reserved"
msgstr "Cette URL d'espace de travail est réservée"
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:123
msgid "This workspace username has already been taken"
msgstr "Ce nom d'utilisateur d'espace de travail est déjà pris"
@@ -1635,7 +1727,11 @@ msgstr "Basculer le menu"
msgid "Track all card changes with detailed activity history."
msgstr "Suivez tous les changements de cartes avec un historique d'activité détaillé."
#: src/views/settings/index.tsx:116
#: src/views/settings/IntegrationsSettings.tsx:73
msgid "Trello"
msgstr "Trello"
#: src/views/settings/IntegrationsSettings.tsx:53
msgid "Trello disconnected"
msgstr "Trello déconnecté"
@@ -1667,7 +1763,7 @@ msgstr "Impossible de créer la liste de contrôle"
msgid "Unable to create list"
msgstr "Impossible de créer une liste"
#: src/components/NewWorkspaceForm.tsx:41
#: src/components/NewWorkspaceForm.tsx:154
msgid "Unable to create workspace"
msgstr "Impossible de créer l'espace de travail"
@@ -1720,16 +1816,16 @@ msgstr "Impossible de mettre à jour l'élément de la liste de contrôle"
msgid "Unable to update comment"
msgstr "Impossible de mettre à jour le commentaire"
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/LabelSelector.tsx:71
msgid "Unable to update labels"
msgstr "Impossible de mettre à jour les étiquettes"
#: src/views/board/index.tsx:133
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/ListSelector.tsx:51
msgid "Unable to update list"
msgstr "Impossible de mettre à jour la liste"
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/MemberSelector.tsx:78
msgid "Unable to update members"
msgstr "Impossible de mettre à jour les membres"
@@ -1766,14 +1862,15 @@ msgstr "Commentaires illimités"
msgid "Unlimited lists"
msgstr "Listes illimitées"
#: src/components/NewWorkspaceForm.tsx:329
#: src/views/settings/components/UpgradeToProConfirmation.tsx:64
msgid "Unlimited members"
msgstr "Membres illimités"
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
#: src/views/settings/components/UpdateDisplayNameForm.tsx:79
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:90
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:82
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:154
msgid "Update"
msgstr "Mettre à jour"
@@ -1799,15 +1896,19 @@ msgstr "a mis à jour le titre"
msgid "updated the title to <0>{0}</0>"
msgstr "a mis à jour le titre en <0>{0}</0>"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:99
#: src/views/settings/components/UpgradeToProConfirmation.tsx:109
msgid "Upgrade"
msgstr "Mettre à niveau"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:52
#: src/views/settings/index.tsx:213
#: src/views/settings/WorkspaceSettings.tsx:89
msgid "Upgrade to Pro"
msgstr "Passer à Pro"
#: src/components/NewWorkspaceForm.tsx:369
msgid "Upgrade to Pro ($29/month)"
msgstr "Passer à Pro (29 $/mois)"
#: src/views/members/components/InviteMemberForm.tsx:224
msgid "Upgrade to Team Plan"
msgstr "Passer au forfait d'équipe"
@@ -1817,14 +1918,17 @@ msgstr "Passer au forfait d'équipe"
msgid "Urgent"
msgstr "Urgent"
#: src/components/NewWorkspaceForm.tsx:35
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:36
msgid "URL can only contain letters, numbers, and hyphens"
msgstr "L'URL ne peut contenir que des lettres, des chiffres et des traits d'union"
#: src/components/NewWorkspaceForm.tsx:33
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:34
msgid "URL cannot exceed 24 characters"
msgstr "L'URL ne peut pas dépasser 24 caractères"
#: src/components/NewWorkspaceForm.tsx:31
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:32
msgid "URL must be at least 3 characters long"
msgstr "L'URL doit comporter au moins 3 caractères"
@@ -1833,7 +1937,7 @@ msgstr "L'URL doit comporter au moins 3 caractères"
msgid "Use template"
msgstr "Utiliser le modèle"
#: src/views/members/index.tsx:217
#: src/views/members/index.tsx:237
msgid "User"
msgstr "Utilisateur"
@@ -1845,11 +1949,11 @@ msgstr "L'utilisateur est déjà membre de cet espace de travail"
msgid "Video"
msgstr "Vidéo"
#: src/views/settings/index.tsx:296
#: src/views/settings/ApiSettings.tsx:25
msgid "View and manage your API keys."
msgstr "Consultez et gérez vos clés API."
#: src/views/settings/index.tsx:235
#: src/views/settings/BillingSettings.tsx:42
msgid "View and manage your billing and subscription."
msgstr "Consultez et gérez votre facturation et votre abonnement."
@@ -1901,15 +2005,20 @@ msgstr "Quand Trello a été lancé en 2011, il a impressionné tout le monde pa
msgid "Why make an open source Trello?"
msgstr "Pourquoi créer un Trello open source ?"
#: src/components/SettingsLayout.tsx:39
#: src/views/board/index.tsx:331
msgid "Workspace"
msgstr "Espace de travail"
#: src/components/NewWorkspaceForm.tsx:143
msgid "Workspace created successfully. You can upgrade later in settings."
msgstr "Espace de travail créé avec succès. Vous pourrez effectuer la mise à niveau ultérieurement dans les paramètres."
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:26
msgid "Workspace deleted"
msgstr "Espace de travail supprimé"
#: src/views/settings/index.tsx:199
#: src/views/settings/WorkspaceSettings.tsx:75
msgid "Workspace description"
msgstr "Description de l'espace de travail"
@@ -1930,8 +2039,8 @@ msgstr "Description de l'espace de travail mise à jour"
msgid "Workspace members"
msgstr "Membres de l'espace de travail"
#: src/components/NewWorkspaceForm.tsx:81
#: src/views/settings/index.tsx:180
#: src/components/NewWorkspaceForm.tsx:259
#: src/views/settings/WorkspaceSettings.tsx:58
msgid "Workspace name"
msgstr "Nom de l'espace de travail"
@@ -1939,6 +2048,10 @@ msgstr "Nom de l'espace de travail"
msgid "Workspace name cannot exceed 24 characters"
msgstr "Le nom de l'espace de travail ne peut pas dépasser 24 caractères"
#: src/components/NewWorkspaceForm.tsx:27
msgid "Workspace name is required"
msgstr "Le nom de l'espace de travail est requis"
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:14
msgid "Workspace name must be at least 3 characters long"
msgstr "Le nom de l'espace de travail doit comporter au moins 3 caractères"
@@ -1951,10 +2064,14 @@ msgstr "Nom de l'espace de travail mis à jour"
msgid "Workspace slug updated"
msgstr "Slug de l'espace de travail mis à jour"
#: src/views/settings/index.tsx:189
#: src/views/settings/WorkspaceSettings.tsx:66
msgid "Workspace URL"
msgstr "URL de l'espace de travail"
#: src/components/NewWorkspaceForm.tsx:273
msgid "workspace-url"
msgstr "workspace-url"
#: src/views/boards/components/TemplateBoards.tsx:30
msgid "Writing"
msgstr "Écriture"
@@ -1967,7 +2084,7 @@ msgstr "Annuel"
msgid "Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits."
msgstr "Oui, nous proposons un plan gratuit à vie pour un usage individuel. Aucune restriction, aucun paywall, aucune limite."
#: src/views/settings/index.tsx:328
#: src/views/settings/AccountSettings.tsx:73
msgid "You are about to change your password."
msgstr "Vous êtes sur le point de modifier votre mot de passe."
@@ -2011,15 +2128,15 @@ msgstr "Votre nom d'affichage a été mis à jour."
msgid "Your password has been changed."
msgstr "Votre mot de passe a été modifié."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Votre image de profil a été mise à jour."
#: src/views/settings/index.tsx:117
#: src/views/settings/IntegrationsSettings.tsx:54
msgid "Your Trello account has been disconnected."
msgstr "Votre compte Trello a été déconnecté."
#: src/views/settings/index.tsx:278
#: src/views/settings/IntegrationsSettings.tsx:102
msgid "Your Trello account is connected."
msgstr "Votre compte Trello est connecté."

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
export const locales = ["en", "fr", "de", "es", "it", "nl"] as const;
export const locales = ["en", "fr", "de", "es", "it", "nl", "ru"] as const;
export type Locale = (typeof locales)[number];
@@ -11,4 +11,5 @@ export const localeNames: Record<Locale, string> = {
es: "Español",
it: "Italiano",
nl: "Nederlands",
ru: "Русский",
};

View File

@@ -53,6 +53,10 @@ msgstr "1 utente"
msgid "A powerful, flexible kanban app that helps you organise work, track progress, and deliver results—all in one place."
msgstr "Un'app kanban potente e flessibile che ti aiuta a organizzare il lavoro, monitorare i progressi e ottenere risultati, tutto in un unico posto."
#: src/components/SettingsLayout.tsx:33
msgid "Account"
msgstr "Account"
#: src/views/settings/components/DeleteAccountConfirmation.tsx:27
msgid "Account deleted"
msgstr "Account eliminato"
@@ -91,8 +95,8 @@ msgstr "Aggiungi descrizione... (digita '/' per aprire i comandi o '@' per menzi
msgid "Add details..."
msgstr "Aggiungi dettagli..."
#: src/views/card/components/LabelSelector.tsx:110
#: src/views/card/components/LabelSelector.tsx:118
#: src/views/card/components/LabelSelector.tsx:109
#: src/views/card/components/LabelSelector.tsx:114
msgid "Add label"
msgstr "Aggiungi etichetta"
@@ -136,6 +140,10 @@ msgstr "ha aggiunto l'etichetta <0>{0}</0>"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'aggiunta di un nuovo membro costerà un supplemento di {price} ({billingType}) per posto."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Regola il ritaglio quadrato per adattarlo al tuo avatar."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Ruoli amministratore"
@@ -148,7 +156,7 @@ msgstr "Tutti i sistemi operativi"
msgid "Already have an account? <0><1>Sign in</1></0>"
msgstr "Hai già un account? <0><1>Accedi</1></0>"
#: src/views/settings/index.tsx:124
#: src/views/settings/IntegrationsSettings.tsx:61
msgid "An error occurred while disconnecting your Trello account."
msgstr "Si è verificato un errore durante la disconnessione del tuo account Trello."
@@ -156,7 +164,27 @@ msgstr "Si è verificato un errore durante la disconnessione del tuo account Tre
msgid "An unexpected error occurred. Please try again later."
msgstr "Si è verificato un errore imprevisto. Riprova più tardi."
#: src/views/settings/index.tsx:293
#: src/components/SettingsLayout.tsx:51
msgid "API"
msgstr "API"
#: src/views/settings/components/NewApiKeyModal.tsx:91
msgid "API key created"
msgstr "Chiave API creata"
#: src/views/settings/components/NewApiKeyModal.tsx:161
msgid "API key name"
msgstr "Nome chiave API"
#: src/views/settings/components/NewApiKeyModal.tsx:25
msgid "API key name cannot exceed 30 characters"
msgstr "Il nome della chiave API non può superare i 30 caratteri"
#: src/views/settings/components/NewApiKeyModal.tsx:24
msgid "API key name is required"
msgstr "Il nome della chiave API è obbligatorio"
#: src/views/settings/ApiSettings.tsx:22
msgid "API keys"
msgstr "Chiavi API"
@@ -226,12 +254,13 @@ msgstr "fatturato annualmente"
msgid "billed monthly"
msgstr "fatturato mensilmente"
#: src/components/SettingsLayout.tsx:44
#: src/views/boards/components/TemplateBoards.tsx:55
#: src/views/settings/index.tsx:232
#: src/views/settings/BillingSettings.tsx:39
msgid "Billing"
msgstr "Fatturazione"
#: src/views/settings/index.tsx:242
#: src/views/settings/BillingSettings.tsx:49
msgid "Billing portal"
msgstr "Portale di fatturazione"
@@ -243,9 +272,10 @@ msgstr "Post del blog"
msgid "Board"
msgstr "Bacheca"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:80
msgid "Board analytics (coming soon)"
msgstr "Analisi della bacheca (prossimamente)"
#: src/components/NewWorkspaceForm.tsx:346
#: src/views/settings/components/UpgradeToProConfirmation.tsx:81
msgid "Board analytics"
msgstr "Analisi della bacheca"
#: src/views/boards/components/NewBoardForm.tsx:21
msgid "Board name cannot exceed 100 characters"
@@ -285,12 +315,12 @@ msgid "Board visibility updated"
msgstr "Visibilità della bacheca aggiornata"
#: src/components/SideNavigation.tsx:68
#: src/views/boards/index.tsx:27
#: src/views/boards/index.tsx:32
msgid "Boards"
msgstr "Bacheche"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/boards/index.tsx:23
#: src/views/boards/index.tsx:28
msgid "Boards | {0}"
msgstr "Bacheche | {0}"
@@ -312,10 +342,11 @@ msgstr "Segnalazione bug"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
#: src/views/settings/components/UpgradeToProConfirmation.tsx:94
#: src/views/settings/components/UpgradeToProConfirmation.tsx:104
msgid "Cancel"
msgstr "Annulla"
@@ -327,16 +358,16 @@ msgstr "Carta non trovata"
msgid "Card title"
msgstr "Titolo della carta"
#: src/views/settings/AccountSettings.tsx:70
#: src/views/settings/AccountSettings.tsx:80
#: src/views/settings/components/ChangePasswordConfirmation.tsx:109
#: src/views/settings/components/ChangePasswordConfirmation.tsx:178
#: src/views/settings/index.tsx:325
#: src/views/settings/index.tsx:335
msgid "Change Password"
msgstr "Cambia password"
#: src/views/settings/index.tsx:224
msgid "Change the language of the app."
msgstr "Cambia la lingua dell'app."
#: src/views/settings/AccountSettings.tsx:45
msgid "Change your language preferences."
msgstr "Modifica le tue preferenze di lingua."
#: src/views/auth/login/index.tsx:41
#: src/views/auth/signup/index.tsx:67
@@ -356,6 +387,10 @@ msgstr "Cancella filtri"
msgid "Click on the link we've sent to {magicLinkRecipient} to sign in."
msgstr "Clicca sul link che abbiamo inviato a {magicLinkRecipient} per accedere."
#: src/views/settings/components/NewApiKeyModal.tsx:136
msgid "Close"
msgstr "Chiudi"
#: src/views/boards/components/TemplateBoards.tsx:22
msgid "Code Review"
msgstr "Revisione del codice"
@@ -364,7 +399,9 @@ msgstr "Revisione del codice"
msgid "Collaborate seamlessly with your team."
msgstr "Collabora senza problemi con il tuo team."
#: src/components/NewWorkspaceForm.tsx:349
#: src/views/home/components/Features.tsx:66
#: src/views/settings/components/UpgradeToProConfirmation.tsx:84
msgid "Coming soon"
msgstr "Prossimamente"
@@ -398,7 +435,7 @@ msgid "Confirm your new password"
msgstr "Conferma la tua nuova password"
#: src/views/boards/components/ImportBoardsForm.tsx:157
#: src/views/settings/index.tsx:269
#: src/views/settings/IntegrationsSettings.tsx:93
msgid "Connect Trello"
msgstr "Connetti Trello"
@@ -406,7 +443,7 @@ msgstr "Connetti Trello"
msgid "Connect your favorite tools to streamline your workflow."
msgstr "Connetti i tuoi strumenti preferiti per semplificare il tuo flusso di lavoro."
#: src/views/settings/index.tsx:256
#: src/views/settings/IntegrationsSettings.tsx:80
msgid "Connect your Trello account to import boards."
msgstr "Connetti il tuo account Trello per importare le bacheche."
@@ -441,6 +478,10 @@ msgstr "Controlla chi può visualizzare e modificare le tue bacheche."
msgid "Create another"
msgstr "Crea un altro"
#: src/views/settings/components/NewApiKeyModal.tsx:175
msgid "Create API key"
msgstr "Crea chiave API"
#: src/views/boards/components/NewBoardForm.tsx:128
msgid "Create board"
msgstr "Crea bacheca"
@@ -465,12 +506,12 @@ msgstr "Crea lista"
msgid "Create new board"
msgstr "Crea nuova bacheca"
#: src/views/settings/components/CreateAPIKeyForm.tsx:54
#: src/views/settings/ApiSettings.tsx:30
msgid "Create new key"
msgstr "Crea nuova chiave"
#: src/views/board/components/NewCardForm.tsx:394
#: src/views/card/components/LabelSelector.tsx:98
#: src/views/card/components/LabelSelector.tsx:97
msgid "Create new label"
msgstr "Crea nuova etichetta"
@@ -478,7 +519,7 @@ msgstr "Crea nuova etichetta"
msgid "Create new list"
msgstr "Crea nuova lista"
#: src/components/NewWorkspaceForm.tsx:94
#: src/components/NewWorkspaceForm.tsx:384
#: src/components/WorkspaceMenu.tsx:116
msgid "Create workspace"
msgstr "Crea spazio di lavoro"
@@ -491,6 +532,10 @@ msgstr "ha creato la carta"
msgid "Critical"
msgstr "Critico"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Ritaglia il tuo avatar"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "La password attuale è obbligatoria"
@@ -499,6 +544,11 @@ msgstr "La password attuale è obbligatoria"
msgid "Custom domain"
msgstr "Dominio personalizzato"
#: src/components/NewWorkspaceForm.tsx:315
msgid "Custom URLs require upgrading to a Pro plan"
msgstr "Gli URL personalizzati richiedono l'aggiornamento a un piano Pro"
#: src/components/NewWorkspaceForm.tsx:339
#: src/views/settings/components/UpgradeToProConfirmation.tsx:74
msgid "Custom workspace URL"
msgstr "URL personalizzato dell'area di lavoro"
@@ -519,9 +569,9 @@ msgstr "Scuro"
msgid "Delete"
msgstr "Elimina"
#: src/views/settings/AccountSettings.tsx:52
#: src/views/settings/AccountSettings.tsx:62
#: src/views/settings/components/DeleteAccountConfirmation.tsx:96
#: src/views/settings/index.tsx:343
#: src/views/settings/index.tsx:353
msgid "Delete account"
msgstr "Elimina account"
@@ -542,8 +592,8 @@ msgid "Delete list"
msgstr "Elimina lista"
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:106
#: src/views/settings/index.tsx:306
#: src/views/settings/index.tsx:317
#: src/views/settings/WorkspaceSettings.tsx:96
#: src/views/settings/WorkspaceSettings.tsx:107
msgid "Delete workspace"
msgstr "Elimina spazio di lavoro"
@@ -569,7 +619,7 @@ msgstr "ha eliminato l'elemento <0>{0}</0> della checklist"
msgid "Design"
msgstr "Design"
#: src/views/settings/index.tsx:284
#: src/views/settings/IntegrationsSettings.tsx:108
msgid "Disconnect Trello"
msgstr "Disconnetti Trello"
@@ -577,7 +627,7 @@ msgstr "Disconnetti Trello"
msgid "Discuss and collaborate on cards."
msgstr "Discuti e collabora sulle schede."
#: src/views/settings/index.tsx:173
#: src/views/settings/AccountSettings.tsx:35
msgid "Display name"
msgstr "Nome visualizzato"
@@ -695,7 +745,7 @@ msgstr "Errore durante l'eliminazione dell'etichetta"
msgid "Error deleting workspace"
msgstr "Errore durante l'eliminazione dell'area di lavoro"
#: src/views/settings/index.tsx:123
#: src/views/settings/IntegrationsSettings.tsx:60
msgid "Error disconnecting Trello"
msgstr "Errore durante la disconnessione da Trello"
@@ -708,7 +758,7 @@ msgstr "Errore durante l'invito del membro"
msgid "Error updating display name"
msgstr "Errore durante l'aggiornamento del nome visualizzato"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Errore durante l'aggiornamento dell'immagine del profilo"
@@ -729,8 +779,12 @@ msgstr "Errore durante l'aggiornamento dell'URL dell'area di lavoro"
msgid "Error upgrading subscription"
msgstr "Errore nell'aggiornamento dell'abbonamento"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/components/NewWorkspaceForm.tsx:142
msgid "Error upgrading to Pro"
msgstr "Errore durante l'aggiornamento a Pro"
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Errore durante il caricamento dell'immagine del profilo"
@@ -796,7 +850,7 @@ msgid "Free"
msgstr "Gratuito"
#: src/views/members/components/InviteMemberForm.tsx:193
#: src/views/members/index.tsx:189
#: src/views/members/index.tsx:208
msgid "Free Plan"
msgstr "Piano gratuito"
@@ -905,7 +959,7 @@ msgstr "Idee"
msgid "Ideas to improve this page..."
msgstr "Idee per migliorare questa pagina..."
#: src/views/boards/index.tsx:38
#: src/views/boards/index.tsx:43
msgid "Import"
msgstr "Importa"
@@ -943,6 +997,7 @@ msgstr "In corso"
msgid "Individuals"
msgstr "Privati"
#: src/components/SettingsLayout.tsx:57
#: src/views/home/components/Features.tsx:114
msgid "Integrations"
msgstr "Integrazioni"
@@ -955,7 +1010,7 @@ msgstr "Colloquio"
msgid "Invalid email address"
msgstr "Indirizzo email non valido"
#: src/views/members/index.tsx:201
#: src/views/members/index.tsx:221
msgid "Invite"
msgstr "Invita"
@@ -963,7 +1018,7 @@ msgstr "Invita"
msgid "Invite another"
msgstr "Invita un altro"
#: src/views/card/components/MemberSelector.tsx:112
#: src/views/card/components/MemberSelector.tsx:111
#: src/views/members/components/InviteMemberForm.tsx:233
msgid "Invite member"
msgstr "Invita membro"
@@ -999,15 +1054,20 @@ msgstr "Etichette"
msgid "Labels & Filters"
msgstr "Etichette & Filtri"
#: src/views/settings/index.tsx:221
#: src/views/settings/AccountSettings.tsx:42
msgid "Language"
msgstr "Lingua"
#: src/components/NewWorkspaceForm.tsx:332
#: src/views/settings/components/UpgradeToProConfirmation.tsx:67
msgid "Launch offer"
msgstr "Offerta di lancio"
#: src/views/home/components/Pricing.tsx:98
#: src/views/members/index.tsx:191
msgid "Launch offer: Get unlimited members with Pro"
msgstr "Offerta di lancio: ottieni membri illimitati con Pro"
#: src/views/home/components/Pricing.tsx:99
msgid "Launch offer: unlimited seats for just $29/month with Pro"
msgstr "Offerta di lancio: posti illimitati a soli $29/mese con Pro"
@@ -1076,12 +1136,12 @@ msgstr "Media priorità"
#: src/views/board/components/Filters.tsx:93
#: src/views/board/components/NewCardForm.tsx:357
#: src/views/card/index.tsx:125
#: src/views/members/index.tsx:172
#: src/views/members/index.tsx:178
msgid "Members"
msgstr "Membri"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/members/index.tsx:168
#: src/views/members/index.tsx:173
msgid "Members | {0}"
msgstr "Membri | {0}"
@@ -1112,10 +1172,14 @@ msgstr "Nome"
msgid "Need help?"
msgstr "Hai bisogno di aiuto?"
#: src/views/boards/index.tsx:48
#: src/views/boards/index.tsx:53
msgid "New"
msgstr "Nuovo"
#: src/views/settings/components/NewApiKeyModal.tsx:147
msgid "New API key"
msgstr "Nuova chiave API"
#: src/views/boards/components/NewBoardForm.tsx:85
msgid "New board"
msgstr "Nuova bacheca"
@@ -1153,7 +1217,7 @@ msgstr "La nuova password deve essere diversa dalla password attuale"
msgid "New Ticket"
msgstr "Nuovo ticket"
#: src/components/NewWorkspaceForm.tsx:65
#: src/components/NewWorkspaceForm.tsx:240
msgid "New workspace"
msgstr "Nuovo spazio di lavoro"
@@ -1189,11 +1253,11 @@ msgstr "Offerta"
msgid "Onboarding"
msgstr "Inserimento"
#: src/views/settings/index.tsx:346
#: src/views/settings/AccountSettings.tsx:55
msgid "Once you delete your account, there is no going back. This action cannot be undone."
msgstr "Una volta eliminato il tuo account, non si può tornare indietro. Questa azione non può essere annullata."
#: src/views/settings/index.tsx:309
#: src/views/settings/WorkspaceSettings.tsx:99
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
msgstr "Una volta eliminata l'area di lavoro, non si può tornare indietro. Questa azione non può essere annullata."
@@ -1225,11 +1289,15 @@ msgstr "La password deve contenere almeno 8 caratteri"
msgid "Passwords do not match"
msgstr "Le password non corrispondono"
#: src/views/home/components/Pricing.tsx:101
#: src/views/members/index.tsx:134
msgid "Paused"
msgstr "In pausa"
#: src/views/home/components/Pricing.tsx:102
msgid "Payment frequency"
msgstr "Frequenza di pagamento"
#: src/views/members/index.tsx:129
#: src/views/members/index.tsx:134
msgid "Pending"
msgstr "In attesa"
@@ -1262,13 +1330,13 @@ msgstr "Inserisci un nome valido"
msgid "Please enter a valid password"
msgstr "Inserisci una password valida"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Seleziona un file da caricare."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:42
#: src/components/NewWorkspaceForm.tsx:155
#: src/views/board/components/NewCardForm.tsx:174
#: src/views/board/components/NewListForm.tsx:78
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
@@ -1283,9 +1351,9 @@ msgstr "Seleziona un file da caricare."
#: src/views/card/components/DeleteCardConfirmation.tsx:52
#: src/views/card/components/DeleteChecklistConfirmation.tsx:37
#: src/views/card/components/DeleteCommentConfirmation.tsx:45
#: src/views/card/components/LabelSelector.tsx:73
#: src/views/card/components/ListSelector.tsx:53
#: src/views/card/components/MemberSelector.tsx:80
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/NewChecklistForm.tsx:70
#: src/views/card/components/NewChecklistItemForm.tsx:89
#: src/views/card/components/NewCommentForm.tsx:31
@@ -1293,8 +1361,8 @@ msgstr "Seleziona un file da caricare."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1312,7 +1380,7 @@ msgid "Pricing"
msgstr "Prezzi"
#: src/views/home/components/Pricing.tsx:56
#: src/views/settings/components/UpgradeToProConfirmation.tsx:86
#: src/views/settings/components/UpgradeToProConfirmation.tsx:91
msgid "Priority email support"
msgstr "Supporto email prioritario"
@@ -1324,7 +1392,7 @@ msgstr "Informativa sulla privacy"
msgid "Private"
msgstr "Privato"
#: src/views/members/index.tsx:186
#: src/views/members/index.tsx:205
msgid "Pro Plan"
msgstr "Piano Pro"
@@ -1332,11 +1400,11 @@ msgstr "Piano Pro"
msgid "Pro Plan ∞"
msgstr "Piano Pro ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Immagine del profilo aggiornata"
#: src/views/settings/index.tsx:168
#: src/views/settings/AccountSettings.tsx:29
msgid "Profile picture"
msgstr "Immagine del profilo"
@@ -1372,7 +1440,7 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Rimuovi"
#: src/views/members/index.tsx:143
#: src/views/members/index.tsx:148
msgid "Remove member"
msgstr "Rimuovi membro"
@@ -1419,16 +1487,12 @@ msgstr "Risorse"
msgid "Review"
msgstr "Revisione"
#: src/views/settings/components/CreateAPIKeyForm.tsx:49
msgid "Revoke"
msgstr "Revoca"
#: src/views/home/components/Footer.tsx:36
#: src/views/home/components/Header.tsx:13
msgid "Roadmap"
msgstr "Roadmap"
#: src/views/members/index.tsx:223
#: src/views/members/index.tsx:243
msgid "Role"
msgstr "Ruolo"
@@ -1437,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Esegui sulla tua infrastruttura"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Salva"
@@ -1476,15 +1541,30 @@ msgstr "Invia feedback"
msgid "Senior"
msgstr "Senior"
#: src/components/SettingsLayout.tsx:82
#: src/components/SideNavigation.tsx:78
#: src/views/settings/index.tsx:162
msgid "Settings"
msgstr "Impostazioni"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/settings/index.tsx:158
msgid "Settings | {0}"
msgstr "Impostazioni | {0}"
#: src/views/settings/AccountSettings.tsx:25
msgid "Settings | Account"
msgstr "Impostazioni | Account"
#: src/views/settings/ApiSettings.tsx:18
msgid "Settings | API"
msgstr "Impostazioni | API"
#: src/views/settings/BillingSettings.tsx:35
msgid "Settings | Billing"
msgstr "Impostazioni | Fatturazione"
#: src/views/settings/IntegrationsSettings.tsx:69
msgid "Settings | Integrations"
msgstr "Impostazioni | Integrazioni"
#: src/views/settings/WorkspaceSettings.tsx:54
msgid "Settings | Workspace"
msgstr "Impostazioni | Area di lavoro"
#: src/views/home/components/Header.tsx:100
#: src/views/home/components/Header.tsx:138
@@ -1550,7 +1630,7 @@ msgid "System"
msgstr "Sistema"
#: src/views/members/components/InviteMemberForm.tsx:182
#: src/views/members/index.tsx:188
#: src/views/members/index.tsx:207
msgid "Team Plan"
msgstr "Piano Team"
@@ -1602,6 +1682,10 @@ msgstr "Non potranno accedere a questo spazio di lavoro."
msgid "This action can't be undone."
msgstr "Questa azione non può essere annullata."
#: src/views/settings/components/NewApiKeyModal.tsx:129
msgid "This API key will only be shown once. Please save it in a secure location."
msgstr "Questa chiave API verrà mostrata una sola volta. Salvala in un luogo sicuro."
#: src/views/public/board/index.tsx:151
msgid "This board is private or does not exist"
msgstr "Questa bacheca è privata o non esiste"
@@ -1618,6 +1702,14 @@ msgstr "Questo comporterà l'eliminazione permanente di tutti i dati associati a
msgid "This will result in the permanent deletion of all data associated with your account."
msgstr "Questo comporterà l'eliminazione permanente di tutti i dati associati al tuo account."
#: src/components/NewWorkspaceForm.tsx:284
msgid "This workspace URL has already been taken"
msgstr "Questo URL dello spazio di lavoro è già stato utilizzato"
#: src/components/NewWorkspaceForm.tsx:286
msgid "This workspace URL is reserved"
msgstr "Questo URL dello spazio di lavoro è riservato"
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:123
msgid "This workspace username has already been taken"
msgstr "Questo nome utente dell'area di lavoro è già stato utilizzato"
@@ -1635,7 +1727,11 @@ msgstr "Attiva/disattiva menu"
msgid "Track all card changes with detailed activity history."
msgstr "Tieni traccia di tutte le modifiche alle schede con una cronologia dettagliata delle attività."
#: src/views/settings/index.tsx:116
#: src/views/settings/IntegrationsSettings.tsx:73
msgid "Trello"
msgstr "Trello"
#: src/views/settings/IntegrationsSettings.tsx:53
msgid "Trello disconnected"
msgstr "Trello disconnesso"
@@ -1667,7 +1763,7 @@ msgstr "Impossibile creare la checklist"
msgid "Unable to create list"
msgstr "Impossibile creare la lista"
#: src/components/NewWorkspaceForm.tsx:41
#: src/components/NewWorkspaceForm.tsx:154
msgid "Unable to create workspace"
msgstr "Impossibile creare lo spazio di lavoro"
@@ -1720,16 +1816,16 @@ msgstr "Impossibile aggiornare l'elemento della checklist"
msgid "Unable to update comment"
msgstr "Impossibile aggiornare il commento"
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/LabelSelector.tsx:71
msgid "Unable to update labels"
msgstr "Impossibile aggiornare le etichette"
#: src/views/board/index.tsx:133
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/ListSelector.tsx:51
msgid "Unable to update list"
msgstr "Impossibile aggiornare la lista"
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/MemberSelector.tsx:78
msgid "Unable to update members"
msgstr "Impossibile aggiornare i membri"
@@ -1766,14 +1862,15 @@ msgstr "Commenti illimitati"
msgid "Unlimited lists"
msgstr "Liste illimitate"
#: src/components/NewWorkspaceForm.tsx:329
#: src/views/settings/components/UpgradeToProConfirmation.tsx:64
msgid "Unlimited members"
msgstr "Membri illimitati"
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
#: src/views/settings/components/UpdateDisplayNameForm.tsx:79
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:90
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:82
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:154
msgid "Update"
msgstr "Aggiorna"
@@ -1799,15 +1896,19 @@ msgstr "ha aggiornato il titolo"
msgid "updated the title to <0>{0}</0>"
msgstr "ha aggiornato il titolo in <0>{0}</0>"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:99
#: src/views/settings/components/UpgradeToProConfirmation.tsx:109
msgid "Upgrade"
msgstr "Aggiorna"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:52
#: src/views/settings/index.tsx:213
#: src/views/settings/WorkspaceSettings.tsx:89
msgid "Upgrade to Pro"
msgstr "Passa a Pro"
#: src/components/NewWorkspaceForm.tsx:369
msgid "Upgrade to Pro ($29/month)"
msgstr "Passa a Pro ($29/mese)"
#: src/views/members/components/InviteMemberForm.tsx:224
msgid "Upgrade to Team Plan"
msgstr "Passa al Piano Team"
@@ -1817,14 +1918,17 @@ msgstr "Passa al Piano Team"
msgid "Urgent"
msgstr "Urgente"
#: src/components/NewWorkspaceForm.tsx:35
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:36
msgid "URL can only contain letters, numbers, and hyphens"
msgstr "L'URL può contenere solo lettere, numeri e trattini"
#: src/components/NewWorkspaceForm.tsx:33
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:34
msgid "URL cannot exceed 24 characters"
msgstr "L'URL non può superare i 24 caratteri"
#: src/components/NewWorkspaceForm.tsx:31
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:32
msgid "URL must be at least 3 characters long"
msgstr "L'URL deve contenere almeno 3 caratteri"
@@ -1833,7 +1937,7 @@ msgstr "L'URL deve contenere almeno 3 caratteri"
msgid "Use template"
msgstr "Usa template"
#: src/views/members/index.tsx:217
#: src/views/members/index.tsx:237
msgid "User"
msgstr "Utente"
@@ -1845,11 +1949,11 @@ msgstr "L'utente è già membro di questo spazio di lavoro"
msgid "Video"
msgstr "Video"
#: src/views/settings/index.tsx:296
#: src/views/settings/ApiSettings.tsx:25
msgid "View and manage your API keys."
msgstr "Visualizza e gestisci le tue chiavi API."
#: src/views/settings/index.tsx:235
#: src/views/settings/BillingSettings.tsx:42
msgid "View and manage your billing and subscription."
msgstr "Visualizza e gestisci la tua fatturazione e abbonamento."
@@ -1901,15 +2005,20 @@ msgstr "Quando Trello fu lanciato nel 2011, stupì tutti con la sua semplicità
msgid "Why make an open source Trello?"
msgstr "Perché creare un Trello open source?"
#: src/components/SettingsLayout.tsx:39
#: src/views/board/index.tsx:331
msgid "Workspace"
msgstr "Spazio di lavoro"
#: src/components/NewWorkspaceForm.tsx:143
msgid "Workspace created successfully. You can upgrade later in settings."
msgstr "Spazio di lavoro creato con successo. Puoi effettuare l'aggiornamento più tardi nelle impostazioni."
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:26
msgid "Workspace deleted"
msgstr "Spazio di lavoro eliminato"
#: src/views/settings/index.tsx:199
#: src/views/settings/WorkspaceSettings.tsx:75
msgid "Workspace description"
msgstr "Descrizione dello spazio di lavoro"
@@ -1930,8 +2039,8 @@ msgstr "Descrizione dell'area di lavoro aggiornata"
msgid "Workspace members"
msgstr "Membri del workspace"
#: src/components/NewWorkspaceForm.tsx:81
#: src/views/settings/index.tsx:180
#: src/components/NewWorkspaceForm.tsx:259
#: src/views/settings/WorkspaceSettings.tsx:58
msgid "Workspace name"
msgstr "Nome dello spazio di lavoro"
@@ -1939,6 +2048,10 @@ msgstr "Nome dello spazio di lavoro"
msgid "Workspace name cannot exceed 24 characters"
msgstr "Il nome dello spazio di lavoro non può superare i 24 caratteri"
#: src/components/NewWorkspaceForm.tsx:27
msgid "Workspace name is required"
msgstr "Il nome dello spazio di lavoro è obbligatorio"
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:14
msgid "Workspace name must be at least 3 characters long"
msgstr "Il nome dello spazio di lavoro deve contenere almeno 3 caratteri"
@@ -1951,10 +2064,14 @@ msgstr "Nome dello spazio di lavoro aggiornato"
msgid "Workspace slug updated"
msgstr "Slug dell'area di lavoro aggiornato"
#: src/views/settings/index.tsx:189
#: src/views/settings/WorkspaceSettings.tsx:66
msgid "Workspace URL"
msgstr "URL dello spazio di lavoro"
#: src/components/NewWorkspaceForm.tsx:273
msgid "workspace-url"
msgstr "workspace-url"
#: src/views/boards/components/TemplateBoards.tsx:30
msgid "Writing"
msgstr "Scrittura"
@@ -1967,7 +2084,7 @@ msgstr "Annuale"
msgid "Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits."
msgstr "Sì, offriamo un piano gratuito per sempre per uso individuale. Nessuna restrizione, nessun paywall, nessun limite."
#: src/views/settings/index.tsx:328
#: src/views/settings/AccountSettings.tsx:73
msgid "You are about to change your password."
msgstr "Stai per cambiare la tua password."
@@ -2011,15 +2128,15 @@ msgstr "Il tuo nome visualizzato è stato aggiornato."
msgid "Your password has been changed."
msgstr "La tua password è stata modificata."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "La tua immagine del profilo è stata aggiornata."
#: src/views/settings/index.tsx:117
#: src/views/settings/IntegrationsSettings.tsx:54
msgid "Your Trello account has been disconnected."
msgstr "Il tuo account Trello è stato disconnesso."
#: src/views/settings/index.tsx:278
#: src/views/settings/IntegrationsSettings.tsx:102
msgid "Your Trello account is connected."
msgstr "Il tuo account Trello è connesso."

File diff suppressed because one or more lines are too long

View File

@@ -53,6 +53,10 @@ msgstr "1 gebruiker"
msgid "A powerful, flexible kanban app that helps you organise work, track progress, and deliver results—all in one place."
msgstr "Een krachtige, flexibele kanban-app die je helpt werk te organiseren, voortgang bij te houden en resultaten te leveren—allemaal op één plek."
#: src/components/SettingsLayout.tsx:33
msgid "Account"
msgstr "Account"
#: src/views/settings/components/DeleteAccountConfirmation.tsx:27
msgid "Account deleted"
msgstr "Account verwijderd"
@@ -91,8 +95,8 @@ msgstr "Beschrijving toevoegen... (typ '/' om commando's te openen of '@' om te
msgid "Add details..."
msgstr "Details toevoegen..."
#: src/views/card/components/LabelSelector.tsx:110
#: src/views/card/components/LabelSelector.tsx:118
#: src/views/card/components/LabelSelector.tsx:109
#: src/views/card/components/LabelSelector.tsx:114
msgid "Add label"
msgstr "Label toevoegen"
@@ -136,6 +140,10 @@ msgstr "heeft label <0>{0}</0> toegevoegd"
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Het toevoegen van een nieuw lid kost een extra {price} ({billingType}) per plaats."
#: src/views/settings/components/Avatar.tsx:278
msgid "Adjust the square crop to fit your avatar."
msgstr "Pas de vierkante uitsnede aan zodat je avatar goed past."
#: src/views/home/components/Pricing.tsx:55
msgid "Admin roles"
msgstr "Beheerdersrollen"
@@ -148,7 +156,7 @@ msgstr "Alle systemen operationeel"
msgid "Already have an account? <0><1>Sign in</1></0>"
msgstr "Heb je al een account? <0><1>Log in</1></0>"
#: src/views/settings/index.tsx:124
#: src/views/settings/IntegrationsSettings.tsx:61
msgid "An error occurred while disconnecting your Trello account."
msgstr "Er is een fout opgetreden bij het verbreken van de verbinding met je Trello-account."
@@ -156,7 +164,27 @@ msgstr "Er is een fout opgetreden bij het verbreken van de verbinding met je Tre
msgid "An unexpected error occurred. Please try again later."
msgstr "Er is een onverwachte fout opgetreden. Probeer het later opnieuw."
#: src/views/settings/index.tsx:293
#: src/components/SettingsLayout.tsx:51
msgid "API"
msgstr "API"
#: src/views/settings/components/NewApiKeyModal.tsx:91
msgid "API key created"
msgstr "API-sleutel aangemaakt"
#: src/views/settings/components/NewApiKeyModal.tsx:161
msgid "API key name"
msgstr "API-sleutelnaam"
#: src/views/settings/components/NewApiKeyModal.tsx:25
msgid "API key name cannot exceed 30 characters"
msgstr "API-sleutelnaam mag niet langer zijn dan 30 tekens"
#: src/views/settings/components/NewApiKeyModal.tsx:24
msgid "API key name is required"
msgstr "API-sleutelnaam is verplicht"
#: src/views/settings/ApiSettings.tsx:22
msgid "API keys"
msgstr "API-sleutels"
@@ -226,12 +254,13 @@ msgstr "jaarlijks gefactureerd"
msgid "billed monthly"
msgstr "maandelijks gefactureerd"
#: src/components/SettingsLayout.tsx:44
#: src/views/boards/components/TemplateBoards.tsx:55
#: src/views/settings/index.tsx:232
#: src/views/settings/BillingSettings.tsx:39
msgid "Billing"
msgstr "Facturering"
#: src/views/settings/index.tsx:242
#: src/views/settings/BillingSettings.tsx:49
msgid "Billing portal"
msgstr "Factureringsportaal"
@@ -243,9 +272,10 @@ msgstr "Blogbericht"
msgid "Board"
msgstr "Bord"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:80
msgid "Board analytics (coming soon)"
msgstr "Bordanalyses (binnenkort beschikbaar)"
#: src/components/NewWorkspaceForm.tsx:346
#: src/views/settings/components/UpgradeToProConfirmation.tsx:81
msgid "Board analytics"
msgstr "Bordanalyses"
#: src/views/boards/components/NewBoardForm.tsx:21
msgid "Board name cannot exceed 100 characters"
@@ -285,12 +315,12 @@ msgid "Board visibility updated"
msgstr "Zichtbaarheid van bord bijgewerkt"
#: src/components/SideNavigation.tsx:68
#: src/views/boards/index.tsx:27
#: src/views/boards/index.tsx:32
msgid "Boards"
msgstr "Borden"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/boards/index.tsx:23
#: src/views/boards/index.tsx:28
msgid "Boards | {0}"
msgstr "Borden | {0}"
@@ -312,10 +342,11 @@ msgstr "Bugrapport"
#: src/views/card/components/DeleteChecklistConfirmation.tsx:63
#: src/views/card/components/DeleteCommentConfirmation.tsx:76
#: src/views/members/components/DeleteMemberConfirmation.tsx:55
#: src/views/settings/components/Avatar.tsx:309
#: src/views/settings/components/ChangePasswordConfirmation.tsx:168
#: src/views/settings/components/DeleteAccountConfirmation.tsx:88
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:98
#: src/views/settings/components/UpgradeToProConfirmation.tsx:94
#: src/views/settings/components/UpgradeToProConfirmation.tsx:104
msgid "Cancel"
msgstr "Annuleren"
@@ -327,16 +358,16 @@ msgstr "Kaart niet gevonden"
msgid "Card title"
msgstr "Kaarttitel"
#: src/views/settings/AccountSettings.tsx:70
#: src/views/settings/AccountSettings.tsx:80
#: src/views/settings/components/ChangePasswordConfirmation.tsx:109
#: src/views/settings/components/ChangePasswordConfirmation.tsx:178
#: src/views/settings/index.tsx:325
#: src/views/settings/index.tsx:335
msgid "Change Password"
msgstr "Wachtwoord wijzigen"
#: src/views/settings/index.tsx:224
msgid "Change the language of the app."
msgstr "Wijzig de taal van de app."
#: src/views/settings/AccountSettings.tsx:45
msgid "Change your language preferences."
msgstr "Wijzig je taalvoorkeuren."
#: src/views/auth/login/index.tsx:41
#: src/views/auth/signup/index.tsx:67
@@ -356,6 +387,10 @@ msgstr "Filters wissen"
msgid "Click on the link we've sent to {magicLinkRecipient} to sign in."
msgstr "Klik op de link die we naar {magicLinkRecipient} hebben gestuurd om in te loggen."
#: src/views/settings/components/NewApiKeyModal.tsx:136
msgid "Close"
msgstr "Sluiten"
#: src/views/boards/components/TemplateBoards.tsx:22
msgid "Code Review"
msgstr "Code review"
@@ -364,7 +399,9 @@ msgstr "Code review"
msgid "Collaborate seamlessly with your team."
msgstr "Werk naadloos samen met je team."
#: src/components/NewWorkspaceForm.tsx:349
#: src/views/home/components/Features.tsx:66
#: src/views/settings/components/UpgradeToProConfirmation.tsx:84
msgid "Coming soon"
msgstr "Binnenkort beschikbaar"
@@ -398,7 +435,7 @@ msgid "Confirm your new password"
msgstr "Bevestig je nieuwe wachtwoord"
#: src/views/boards/components/ImportBoardsForm.tsx:157
#: src/views/settings/index.tsx:269
#: src/views/settings/IntegrationsSettings.tsx:93
msgid "Connect Trello"
msgstr "Verbind Trello"
@@ -406,7 +443,7 @@ msgstr "Verbind Trello"
msgid "Connect your favorite tools to streamline your workflow."
msgstr "Verbind je favoriete tools om je werkstroom te stroomlijnen."
#: src/views/settings/index.tsx:256
#: src/views/settings/IntegrationsSettings.tsx:80
msgid "Connect your Trello account to import boards."
msgstr "Verbind je Trello-account om borden te importeren."
@@ -441,6 +478,10 @@ msgstr "Bepaal wie je borden kan bekijken en bewerken."
msgid "Create another"
msgstr "Maak nog een"
#: src/views/settings/components/NewApiKeyModal.tsx:175
msgid "Create API key"
msgstr "API-sleutel aanmaken"
#: src/views/boards/components/NewBoardForm.tsx:128
msgid "Create board"
msgstr "Maak bord"
@@ -465,12 +506,12 @@ msgstr "Lijst maken"
msgid "Create new board"
msgstr "Nieuw bord maken"
#: src/views/settings/components/CreateAPIKeyForm.tsx:54
#: src/views/settings/ApiSettings.tsx:30
msgid "Create new key"
msgstr "Nieuwe sleutel aanmaken"
#: src/views/board/components/NewCardForm.tsx:394
#: src/views/card/components/LabelSelector.tsx:98
#: src/views/card/components/LabelSelector.tsx:97
msgid "Create new label"
msgstr "Maak nieuw label"
@@ -478,7 +519,7 @@ msgstr "Maak nieuw label"
msgid "Create new list"
msgstr "Maak nieuwe lijst"
#: src/components/NewWorkspaceForm.tsx:94
#: src/components/NewWorkspaceForm.tsx:384
#: src/components/WorkspaceMenu.tsx:116
msgid "Create workspace"
msgstr "Werkruimte maken"
@@ -491,6 +532,10 @@ msgstr "heeft de kaart aangemaakt"
msgid "Critical"
msgstr "Kritiek"
#: src/views/settings/components/Avatar.tsx:275
msgid "Crop your avatar"
msgstr "Snijd je avatar bij"
#: src/views/settings/components/ChangePasswordConfirmation.tsx:19
msgid "Current password is required"
msgstr "Huidig wachtwoord is vereist"
@@ -499,6 +544,11 @@ msgstr "Huidig wachtwoord is vereist"
msgid "Custom domain"
msgstr "Aangepast domein"
#: src/components/NewWorkspaceForm.tsx:315
msgid "Custom URLs require upgrading to a Pro plan"
msgstr "Aangepaste URL's vereisen een upgrade naar een Pro-abonnement"
#: src/components/NewWorkspaceForm.tsx:339
#: src/views/settings/components/UpgradeToProConfirmation.tsx:74
msgid "Custom workspace URL"
msgstr "Aangepaste werkruimte-URL"
@@ -519,9 +569,9 @@ msgstr "Donker"
msgid "Delete"
msgstr "Verwijderen"
#: src/views/settings/AccountSettings.tsx:52
#: src/views/settings/AccountSettings.tsx:62
#: src/views/settings/components/DeleteAccountConfirmation.tsx:96
#: src/views/settings/index.tsx:343
#: src/views/settings/index.tsx:353
msgid "Delete account"
msgstr "Account verwijderen"
@@ -542,8 +592,8 @@ msgid "Delete list"
msgstr "Lijst verwijderen"
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:106
#: src/views/settings/index.tsx:306
#: src/views/settings/index.tsx:317
#: src/views/settings/WorkspaceSettings.tsx:96
#: src/views/settings/WorkspaceSettings.tsx:107
msgid "Delete workspace"
msgstr "Werkruimte verwijderen"
@@ -569,7 +619,7 @@ msgstr "heeft checklistitem <0>{0}</0> verwijderd"
msgid "Design"
msgstr "Ontwerp"
#: src/views/settings/index.tsx:284
#: src/views/settings/IntegrationsSettings.tsx:108
msgid "Disconnect Trello"
msgstr "Trello ontkoppelen"
@@ -577,7 +627,7 @@ msgstr "Trello ontkoppelen"
msgid "Discuss and collaborate on cards."
msgstr "Bespreek en werk samen aan kaarten."
#: src/views/settings/index.tsx:173
#: src/views/settings/AccountSettings.tsx:35
msgid "Display name"
msgstr "Weergavenaam"
@@ -695,7 +745,7 @@ msgstr "Fout bij het verwijderen van label"
msgid "Error deleting workspace"
msgstr "Fout bij verwijderen werkruimte"
#: src/views/settings/index.tsx:123
#: src/views/settings/IntegrationsSettings.tsx:60
msgid "Error disconnecting Trello"
msgstr "Fout bij ontkoppelen van Trello"
@@ -708,7 +758,7 @@ msgstr "Fout bij het uitnodigen van lid"
msgid "Error updating display name"
msgstr "Fout bij bijwerken weergavenaam"
#: src/views/settings/components/Avatar.tsx:39
#: src/views/settings/components/Avatar.tsx:80
msgid "Error updating profile image"
msgstr "Fout bij het bijwerken van profielafbeelding"
@@ -729,8 +779,12 @@ msgstr "Fout bij het bijwerken van werkruimte-URL"
msgid "Error upgrading subscription"
msgstr "Fout bij het upgraden van abonnement"
#: src/views/settings/components/Avatar.tsx:56
#: src/views/settings/components/Avatar.tsx:97
#: src/components/NewWorkspaceForm.tsx:142
msgid "Error upgrading to Pro"
msgstr "Fout bij upgraden naar Pro"
#: src/views/settings/components/Avatar.tsx:94
#: src/views/settings/components/Avatar.tsx:221
msgid "Error uploading profile image"
msgstr "Fout bij het uploaden van profielafbeelding"
@@ -796,7 +850,7 @@ msgid "Free"
msgstr "Gratis"
#: src/views/members/components/InviteMemberForm.tsx:193
#: src/views/members/index.tsx:189
#: src/views/members/index.tsx:208
msgid "Free Plan"
msgstr "Gratis plan"
@@ -905,7 +959,7 @@ msgstr "Ideeën"
msgid "Ideas to improve this page..."
msgstr "Ideeën om deze pagina te verbeteren..."
#: src/views/boards/index.tsx:38
#: src/views/boards/index.tsx:43
msgid "Import"
msgstr "Importeren"
@@ -943,6 +997,7 @@ msgstr "In behandeling"
msgid "Individuals"
msgstr "Individuen"
#: src/components/SettingsLayout.tsx:57
#: src/views/home/components/Features.tsx:114
msgid "Integrations"
msgstr "Integraties"
@@ -955,7 +1010,7 @@ msgstr "Interviewen"
msgid "Invalid email address"
msgstr "Ongeldig e-mailadres"
#: src/views/members/index.tsx:201
#: src/views/members/index.tsx:221
msgid "Invite"
msgstr "Uitnodigen"
@@ -963,7 +1018,7 @@ msgstr "Uitnodigen"
msgid "Invite another"
msgstr "Nog iemand uitnodigen"
#: src/views/card/components/MemberSelector.tsx:112
#: src/views/card/components/MemberSelector.tsx:111
#: src/views/members/components/InviteMemberForm.tsx:233
msgid "Invite member"
msgstr "Lid uitnodigen"
@@ -999,15 +1054,20 @@ msgstr "Labels"
msgid "Labels & Filters"
msgstr "Labels & filters"
#: src/views/settings/index.tsx:221
#: src/views/settings/AccountSettings.tsx:42
msgid "Language"
msgstr "Taal"
#: src/components/NewWorkspaceForm.tsx:332
#: src/views/settings/components/UpgradeToProConfirmation.tsx:67
msgid "Launch offer"
msgstr "Introductieaanbieding"
#: src/views/home/components/Pricing.tsx:98
#: src/views/members/index.tsx:191
msgid "Launch offer: Get unlimited members with Pro"
msgstr "Lanceringsaanbieding: krijg onbeperkt aantal leden met Pro"
#: src/views/home/components/Pricing.tsx:99
msgid "Launch offer: unlimited seats for just $29/month with Pro"
msgstr "Introductieaanbieding: onbeperkt aantal gebruikers voor slechts $29/maand met Pro"
@@ -1076,12 +1136,12 @@ msgstr "Gemiddelde prioriteit"
#: src/views/board/components/Filters.tsx:93
#: src/views/board/components/NewCardForm.tsx:357
#: src/views/card/index.tsx:125
#: src/views/members/index.tsx:172
#: src/views/members/index.tsx:178
msgid "Members"
msgstr "Leden"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/members/index.tsx:168
#: src/views/members/index.tsx:173
msgid "Members | {0}"
msgstr "Leden | {0}"
@@ -1112,10 +1172,14 @@ msgstr "Naam"
msgid "Need help?"
msgstr "Hulp nodig?"
#: src/views/boards/index.tsx:48
#: src/views/boards/index.tsx:53
msgid "New"
msgstr "Nieuw"
#: src/views/settings/components/NewApiKeyModal.tsx:147
msgid "New API key"
msgstr "Nieuwe API-sleutel"
#: src/views/boards/components/NewBoardForm.tsx:85
msgid "New board"
msgstr "Nieuw bord"
@@ -1153,7 +1217,7 @@ msgstr "Nieuw wachtwoord moet verschillen van het huidige wachtwoord"
msgid "New Ticket"
msgstr "Nieuw ticket"
#: src/components/NewWorkspaceForm.tsx:65
#: src/components/NewWorkspaceForm.tsx:240
msgid "New workspace"
msgstr "Nieuwe werkruimte"
@@ -1189,11 +1253,11 @@ msgstr "Aanbod"
msgid "Onboarding"
msgstr "Inwerktraject"
#: src/views/settings/index.tsx:346
#: src/views/settings/AccountSettings.tsx:55
msgid "Once you delete your account, there is no going back. This action cannot be undone."
msgstr "Zodra je je account verwijdert, is er geen weg terug. Deze actie kan niet ongedaan worden gemaakt."
#: src/views/settings/index.tsx:309
#: src/views/settings/WorkspaceSettings.tsx:99
msgid "Once you delete your workspace, there is no going back. This action cannot be undone."
msgstr "Zodra je je werkruimte verwijdert, is er geen weg terug. Deze actie kan niet ongedaan worden gemaakt."
@@ -1225,11 +1289,15 @@ msgstr "Wachtwoord moet minimaal 8 tekens bevatten"
msgid "Passwords do not match"
msgstr "Wachtwoorden komen niet overeen"
#: src/views/home/components/Pricing.tsx:101
#: src/views/members/index.tsx:134
msgid "Paused"
msgstr "Gepauzeerd"
#: src/views/home/components/Pricing.tsx:102
msgid "Payment frequency"
msgstr "Betalingsfrequentie"
#: src/views/members/index.tsx:129
#: src/views/members/index.tsx:134
msgid "Pending"
msgstr "In behandeling"
@@ -1262,13 +1330,13 @@ msgstr "Voer een geldige naam in"
msgid "Please enter a valid password"
msgstr "Voer een geldig wachtwoord in"
#: src/views/settings/components/Avatar.tsx:57
#: src/views/settings/components/Avatar.tsx:95
msgid "Please select a file to upload."
msgstr "Selecteer een bestand om te uploaden."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:42
#: src/components/NewWorkspaceForm.tsx:155
#: src/views/board/components/NewCardForm.tsx:174
#: src/views/board/components/NewListForm.tsx:78
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
@@ -1283,9 +1351,9 @@ msgstr "Selecteer een bestand om te uploaden."
#: src/views/card/components/DeleteCardConfirmation.tsx:52
#: src/views/card/components/DeleteChecklistConfirmation.tsx:37
#: src/views/card/components/DeleteCommentConfirmation.tsx:45
#: src/views/card/components/LabelSelector.tsx:73
#: src/views/card/components/ListSelector.tsx:53
#: src/views/card/components/MemberSelector.tsx:80
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/NewChecklistForm.tsx:70
#: src/views/card/components/NewChecklistItemForm.tsx:89
#: src/views/card/components/NewCommentForm.tsx:31
@@ -1293,8 +1361,8 @@ msgstr "Selecteer een bestand om te uploaden."
#: src/views/members/components/DeleteMemberConfirmation.tsx:28
#: src/views/members/components/InviteMemberForm.tsx:78
#: src/views/members/components/InviteMemberForm.tsx:131
#: src/views/settings/components/Avatar.tsx:40
#: src/views/settings/components/Avatar.tsx:98
#: src/views/settings/components/Avatar.tsx:81
#: src/views/settings/components/Avatar.tsx:222
#: src/views/settings/components/DeleteAccountConfirmation.tsx:40
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:46
#: src/views/settings/components/UpdateDisplayNameForm.tsx:55
@@ -1312,7 +1380,7 @@ msgid "Pricing"
msgstr "Prijzen"
#: src/views/home/components/Pricing.tsx:56
#: src/views/settings/components/UpgradeToProConfirmation.tsx:86
#: src/views/settings/components/UpgradeToProConfirmation.tsx:91
msgid "Priority email support"
msgstr "Prioriteit e-mailondersteuning"
@@ -1324,7 +1392,7 @@ msgstr "Privacybeleid"
msgid "Private"
msgstr "Privé"
#: src/views/members/index.tsx:186
#: src/views/members/index.tsx:205
msgid "Pro Plan"
msgstr "Pro Plan"
@@ -1332,11 +1400,11 @@ msgstr "Pro Plan"
msgid "Pro Plan ∞"
msgstr "Pro Plan ∞"
#: src/views/settings/components/Avatar.tsx:26
#: src/views/settings/components/Avatar.tsx:67
msgid "Profile image updated"
msgstr "Profielafbeelding bijgewerkt"
#: src/views/settings/index.tsx:168
#: src/views/settings/AccountSettings.tsx:29
msgid "Profile picture"
msgstr "Profielfoto"
@@ -1372,7 +1440,7 @@ msgstr "Op afstand"
msgid "Remove"
msgstr "Verwijderen"
#: src/views/members/index.tsx:143
#: src/views/members/index.tsx:148
msgid "Remove member"
msgstr "Lid verwijderen"
@@ -1419,16 +1487,12 @@ msgstr "Bronnen"
msgid "Review"
msgstr "Beoordeling"
#: src/views/settings/components/CreateAPIKeyForm.tsx:49
msgid "Revoke"
msgstr "Intrekken"
#: src/views/home/components/Footer.tsx:36
#: src/views/home/components/Header.tsx:13
msgid "Roadmap"
msgstr "Roadmap"
#: src/views/members/index.tsx:223
#: src/views/members/index.tsx:243
msgid "Role"
msgstr "Rol"
@@ -1437,6 +1501,7 @@ msgid "Run on your own infrastructure"
msgstr "Draai op je eigen infrastructuur"
#: src/views/card/components/Comment.tsx:165
#: src/views/settings/components/Avatar.tsx:312
msgid "Save"
msgstr "Opslaan"
@@ -1476,15 +1541,30 @@ msgstr "Feedback versturen"
msgid "Senior"
msgstr "Senior"
#: src/components/SettingsLayout.tsx:82
#: src/components/SideNavigation.tsx:78
#: src/views/settings/index.tsx:162
msgid "Settings"
msgstr "Instellingen"
#. placeholder {0}: workspace.name ?? "Workspace"
#: src/views/settings/index.tsx:158
msgid "Settings | {0}"
msgstr "Instellingen | {0}"
#: src/views/settings/AccountSettings.tsx:25
msgid "Settings | Account"
msgstr "Instellingen | Account"
#: src/views/settings/ApiSettings.tsx:18
msgid "Settings | API"
msgstr "Instellingen | API"
#: src/views/settings/BillingSettings.tsx:35
msgid "Settings | Billing"
msgstr "Instellingen | Facturering"
#: src/views/settings/IntegrationsSettings.tsx:69
msgid "Settings | Integrations"
msgstr "Instellingen | Integraties"
#: src/views/settings/WorkspaceSettings.tsx:54
msgid "Settings | Workspace"
msgstr "Instellingen | Werkruimte"
#: src/views/home/components/Header.tsx:100
#: src/views/home/components/Header.tsx:138
@@ -1550,7 +1630,7 @@ msgid "System"
msgstr "Systeem"
#: src/views/members/components/InviteMemberForm.tsx:182
#: src/views/members/index.tsx:188
#: src/views/members/index.tsx:207
msgid "Team Plan"
msgstr "Teamplan"
@@ -1602,6 +1682,10 @@ msgstr "Ze zullen geen toegang meer hebben tot deze werkruimte."
msgid "This action can't be undone."
msgstr "Deze actie kan niet ongedaan worden gemaakt."
#: src/views/settings/components/NewApiKeyModal.tsx:129
msgid "This API key will only be shown once. Please save it in a secure location."
msgstr "Deze API-sleutel wordt slechts één keer getoond. Bewaar deze op een veilige plaats."
#: src/views/public/board/index.tsx:151
msgid "This board is private or does not exist"
msgstr "Dit bord is privé of bestaat niet"
@@ -1618,6 +1702,14 @@ msgstr "Dit zal resulteren in het permanent verwijderen van alle gegevens die aa
msgid "This will result in the permanent deletion of all data associated with your account."
msgstr "Dit zal resulteren in het permanent verwijderen van alle gegevens die aan je account zijn gekoppeld."
#: src/components/NewWorkspaceForm.tsx:284
msgid "This workspace URL has already been taken"
msgstr "Deze werkruimte-URL is al in gebruik"
#: src/components/NewWorkspaceForm.tsx:286
msgid "This workspace URL is reserved"
msgstr "Deze werkruimte-URL is gereserveerd"
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:123
msgid "This workspace username has already been taken"
msgstr "Deze werkruimtegebruikersnaam is al in gebruik"
@@ -1635,7 +1727,11 @@ msgstr "Menu in-/uitschakelen"
msgid "Track all card changes with detailed activity history."
msgstr "Volg alle kaartwijzigingen met gedetailleerde activiteitengeschiedenis."
#: src/views/settings/index.tsx:116
#: src/views/settings/IntegrationsSettings.tsx:73
msgid "Trello"
msgstr "Trello"
#: src/views/settings/IntegrationsSettings.tsx:53
msgid "Trello disconnected"
msgstr "Trello ontkoppeld"
@@ -1667,7 +1763,7 @@ msgstr "Kan checklist niet maken"
msgid "Unable to create list"
msgstr "Kan geen lijst maken"
#: src/components/NewWorkspaceForm.tsx:41
#: src/components/NewWorkspaceForm.tsx:154
msgid "Unable to create workspace"
msgstr "Kan werkruimte niet aanmaken"
@@ -1720,16 +1816,16 @@ msgstr "Kan checklistitem niet bijwerken"
msgid "Unable to update comment"
msgstr "Kan reactie niet bijwerken"
#: src/views/card/components/LabelSelector.tsx:72
#: src/views/card/components/LabelSelector.tsx:71
msgid "Unable to update labels"
msgstr "Kan labels niet bijwerken"
#: src/views/board/index.tsx:133
#: src/views/card/components/ListSelector.tsx:52
#: src/views/card/components/ListSelector.tsx:51
msgid "Unable to update list"
msgstr "Kan lijst niet bijwerken"
#: src/views/card/components/MemberSelector.tsx:79
#: src/views/card/components/MemberSelector.tsx:78
msgid "Unable to update members"
msgstr "Kan leden niet bijwerken"
@@ -1766,14 +1862,15 @@ msgstr "Onbeperkt aantal opmerkingen"
msgid "Unlimited lists"
msgstr "Onbeperkt aantal lijsten"
#: src/components/NewWorkspaceForm.tsx:329
#: src/views/settings/components/UpgradeToProConfirmation.tsx:64
msgid "Unlimited members"
msgstr "Onbeperkt aantal leden"
#: src/views/board/components/UpdateBoardSlugForm.tsx:174
#: src/views/settings/components/UpdateDisplayNameForm.tsx:79
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:90
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:82
#: src/views/settings/components/UpdateDisplayNameForm.tsx:80
#: src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx:91
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:83
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:154
msgid "Update"
msgstr "Bijwerken"
@@ -1799,15 +1896,19 @@ msgstr "titel bijgewerkt"
msgid "updated the title to <0>{0}</0>"
msgstr "titel bijgewerkt naar <0>{0}</0>"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:99
#: src/views/settings/components/UpgradeToProConfirmation.tsx:109
msgid "Upgrade"
msgstr "Upgraden"
#: src/views/settings/components/UpgradeToProConfirmation.tsx:52
#: src/views/settings/index.tsx:213
#: src/views/settings/WorkspaceSettings.tsx:89
msgid "Upgrade to Pro"
msgstr "Upgraden naar Pro"
#: src/components/NewWorkspaceForm.tsx:369
msgid "Upgrade to Pro ($29/month)"
msgstr "Upgrade naar Pro ($29/maand)"
#: src/views/members/components/InviteMemberForm.tsx:224
msgid "Upgrade to Team Plan"
msgstr "Upgraden naar teamplan"
@@ -1817,14 +1918,17 @@ msgstr "Upgraden naar teamplan"
msgid "Urgent"
msgstr "Urgent"
#: src/components/NewWorkspaceForm.tsx:35
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:36
msgid "URL can only contain letters, numbers, and hyphens"
msgstr "URL kan alleen letters, cijfers en koppeltekens bevatten"
#: src/components/NewWorkspaceForm.tsx:33
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:34
msgid "URL cannot exceed 24 characters"
msgstr "URL mag niet langer zijn dan 24 tekens"
#: src/components/NewWorkspaceForm.tsx:31
#: src/views/settings/components/UpdateWorkspaceUrlForm.tsx:32
msgid "URL must be at least 3 characters long"
msgstr "URL moet minimaal 3 tekens lang zijn"
@@ -1833,7 +1937,7 @@ msgstr "URL moet minimaal 3 tekens lang zijn"
msgid "Use template"
msgstr "Sjabloon gebruiken"
#: src/views/members/index.tsx:217
#: src/views/members/index.tsx:237
msgid "User"
msgstr "Gebruiker"
@@ -1845,11 +1949,11 @@ msgstr "Gebruiker is al lid van deze werkruimte"
msgid "Video"
msgstr "Video"
#: src/views/settings/index.tsx:296
#: src/views/settings/ApiSettings.tsx:25
msgid "View and manage your API keys."
msgstr "Bekijk en beheer je API-sleutels."
#: src/views/settings/index.tsx:235
#: src/views/settings/BillingSettings.tsx:42
msgid "View and manage your billing and subscription."
msgstr "Bekijk en beheer je facturering en abonnement."
@@ -1901,15 +2005,20 @@ msgstr "Toen Trello in 2011 werd gelanceerd, blies het iedereen omver met zijn z
msgid "Why make an open source Trello?"
msgstr "Waarom een open source Trello maken?"
#: src/components/SettingsLayout.tsx:39
#: src/views/board/index.tsx:331
msgid "Workspace"
msgstr "Werkruimte"
#: src/components/NewWorkspaceForm.tsx:143
msgid "Workspace created successfully. You can upgrade later in settings."
msgstr "Werkruimte succesvol aangemaakt. Je kunt later upgraden in de instellingen."
#: src/views/settings/components/DeleteWorkspaceConfirmation.tsx:26
msgid "Workspace deleted"
msgstr "Werkruimte verwijderd"
#: src/views/settings/index.tsx:199
#: src/views/settings/WorkspaceSettings.tsx:75
msgid "Workspace description"
msgstr "Werkruimte beschrijving"
@@ -1930,8 +2039,8 @@ msgstr "Werkruimtebeschrijving bijgewerkt"
msgid "Workspace members"
msgstr "Werkruimteleden"
#: src/components/NewWorkspaceForm.tsx:81
#: src/views/settings/index.tsx:180
#: src/components/NewWorkspaceForm.tsx:259
#: src/views/settings/WorkspaceSettings.tsx:58
msgid "Workspace name"
msgstr "Naam werkruimte"
@@ -1939,6 +2048,10 @@ msgstr "Naam werkruimte"
msgid "Workspace name cannot exceed 24 characters"
msgstr "Werkruimtenaam mag niet langer zijn dan 24 tekens"
#: src/components/NewWorkspaceForm.tsx:27
msgid "Workspace name is required"
msgstr "Naam van werkruimte is verplicht"
#: src/views/settings/components/UpdateWorkspaceNameForm.tsx:14
msgid "Workspace name must be at least 3 characters long"
msgstr "Werkruimtenaam moet minimaal 3 tekens lang zijn"
@@ -1951,10 +2064,14 @@ msgstr "Werkruimtenaam bijgewerkt"
msgid "Workspace slug updated"
msgstr "Werkruimte-slug bijgewerkt"
#: src/views/settings/index.tsx:189
#: src/views/settings/WorkspaceSettings.tsx:66
msgid "Workspace URL"
msgstr "Werkruimte URL"
#: src/components/NewWorkspaceForm.tsx:273
msgid "workspace-url"
msgstr "werkruimte-url"
#: src/views/boards/components/TemplateBoards.tsx:30
msgid "Writing"
msgstr "Schrijven"
@@ -1967,7 +2084,7 @@ msgstr "Jaarlijks"
msgid "Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits."
msgstr "Ja, we bieden een voor altijd gratis plan voor individueel gebruik. Geen beperkingen, geen betaalmuren, geen limieten."
#: src/views/settings/index.tsx:328
#: src/views/settings/AccountSettings.tsx:73
msgid "You are about to change your password."
msgstr "Je staat op het punt je wachtwoord te wijzigen."
@@ -2011,15 +2128,15 @@ msgstr "Je weergavenaam is bijgewerkt."
msgid "Your password has been changed."
msgstr "Je wachtwoord is gewijzigd."
#: src/views/settings/components/Avatar.tsx:27
#: src/views/settings/components/Avatar.tsx:68
msgid "Your profile image has been updated."
msgstr "Je profielafbeelding is bijgewerkt."
#: src/views/settings/index.tsx:117
#: src/views/settings/IntegrationsSettings.tsx:54
msgid "Your Trello account has been disconnected."
msgstr "Je Trello-account is ontkoppeld."
#: src/views/settings/index.tsx:278
#: src/views/settings/IntegrationsSettings.tsx:102
msgid "Your Trello account is connected."
msgstr "Je Trello-account is verbonden."

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ import type { ReactElement, ReactNode } from "react";
import { Plus_Jakarta_Sans } from "next/font/google";
import Script from "next/script";
import { env } from "next-runtime-env";
import { ThemeProvider } from "next-themes";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";
@@ -14,7 +15,6 @@ import { useEffect } from "react";
import { LinguiProviderWrapper } from "~/providers/lingui";
import { ModalProvider } from "~/providers/modal";
import { PopupProvider } from "~/providers/popup";
import { ThemeProvider } from "next-themes";
import { api } from "~/utils/api";
const jakarta = Plus_Jakarta_Sans({
@@ -82,8 +82,8 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
<main className="font-sans">
<LinguiProviderWrapper>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ModalProvider>
<PopupProvider>
<ModalProvider>
<PopupProvider>
{posthogKey ? (
<PostHogProvider client={posthog}>
{getLayout(<Component {...pageProps} />)}
@@ -91,8 +91,8 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
) : (
getLayout(<Component {...pageProps} />)
)}
</PopupProvider>
</ModalProvider>
</PopupProvider>
</ModalProvider>
</ThemeProvider>
</LinguiProviderWrapper>
</main>

View File

@@ -0,0 +1,16 @@
import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout";
import AccountSettings from "~/views/settings/AccountSettings";
const AccountSettingsPage: NextPageWithLayout = () => {
return (
<SettingsLayout currentTab="account">
<AccountSettings />
</SettingsLayout>
);
};
AccountSettingsPage.getLayout = (page) => getDashboardLayout(page);
export default AccountSettingsPage;

View File

@@ -0,0 +1,16 @@
import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout";
import ApiSettings from "~/views/settings/ApiSettings";
const ApiSettingsPage: NextPageWithLayout = () => {
return (
<SettingsLayout currentTab="api">
<ApiSettings />
</SettingsLayout>
);
};
ApiSettingsPage.getLayout = (page) => getDashboardLayout(page);
export default ApiSettingsPage;

View File

@@ -0,0 +1,16 @@
import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout";
import BillingSettings from "~/views/settings/BillingSettings";
const BillingSettingsPage: NextPageWithLayout = () => {
return (
<SettingsLayout currentTab="billing">
<BillingSettings />
</SettingsLayout>
);
};
BillingSettingsPage.getLayout = (page) => getDashboardLayout(page);
export default BillingSettingsPage;

View File

@@ -1,17 +0,0 @@
import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard";
import Popup from "~/components/Popup";
import SettingsView from "~/views/settings";
const SettingsPage: NextPageWithLayout = () => {
return (
<>
<SettingsView />
<Popup />
</>
);
};
SettingsPage.getLayout = (page) => getDashboardLayout(page);
export default SettingsPage;

View File

@@ -0,0 +1,16 @@
import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout";
import IntegrationsSettings from "~/views/settings/IntegrationsSettings";
const IntegrationsSettingsPage: NextPageWithLayout = () => {
return (
<SettingsLayout currentTab="integrations">
<IntegrationsSettings />
</SettingsLayout>
);
};
IntegrationsSettingsPage.getLayout = (page) => getDashboardLayout(page);
export default IntegrationsSettingsPage;

View File

@@ -0,0 +1,16 @@
import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout";
import WorkspaceSettings from "~/views/settings/WorkspaceSettings";
const WorkspaceSettingsPage: NextPageWithLayout = () => {
return (
<SettingsLayout currentTab="workspace">
<WorkspaceSettings />
</SettingsLayout>
);
};
WorkspaceSettingsPage.getLayout = (page) => getDashboardLayout(page);
export default WorkspaceSettingsPage;

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useState } from "react";
import { createContext, useCallback, useContext, useState } from "react";
interface ModalState {
contentType: string;
@@ -42,57 +42,59 @@ export const ModalProvider: React.FC<Props> = ({ children }) => {
const entityId = currentModal?.entityId || "";
const entityLabel = currentModal?.entityLabel || "";
const openModal = (
contentType: string,
entityId?: string,
entityLabel?: string,
) => {
const newModal: ModalState = { contentType, entityId, entityLabel };
setModalStack((prev) => [...prev, newModal]);
};
const openModal = useCallback(
(contentType: string, entityId?: string, entityLabel?: string) => {
const newModal: ModalState = { contentType, entityId, entityLabel };
setModalStack((prev) => [...prev, newModal]);
},
[],
);
const closeModal = () => {
const closeModal = useCallback(() => {
setModalStack((prev) => {
if (prev.length <= 1) {
return [];
}
return prev.slice(0, -1);
});
};
}, []);
const closeModals = (count: number) => {
setModalStack(prev => {
const closeModals = useCallback((count: number) => {
setModalStack((prev) => {
const newLength = Math.max(0, prev.length - count);
return prev.slice(0, newLength);
});
};
}, []);
const clearAllModals = () => {
const clearAllModals = useCallback(() => {
setModalStack([]);
};
}, []);
const setModalState = (modalType: string, state: any) => {
const setModalState = useCallback((modalType: string, state: any) => {
setModalStates((prev) => ({
...prev,
[modalType]: state,
}));
};
}, []);
const getModalState = (modalType: string) => {
return modalStates[modalType];
};
const getModalState = useCallback(
(modalType: string) => {
return modalStates[modalType];
},
[modalStates],
);
const clearModalState = (modalType: string) => {
const clearModalState = useCallback((modalType: string) => {
setModalStates((prev) => {
const newStates = { ...prev };
delete newStates[modalType];
return newStates;
});
};
}, []);
const clearAllModalStates = () => {
const clearAllModalStates = useCallback(() => {
setModalStates({});
};
}, []);
return (
<ModalContext.Provider

View File

@@ -47,12 +47,16 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
const [hasLoaded, setHasLoaded] = useState(false);
const { data, isLoading } = api.workspace.all.useQuery();
const utils = api.useUtils();
const switchWorkspace = (_workspace: Workspace) => {
localStorage.setItem("workspacePublicId", _workspace.publicId);
setWorkspace(_workspace);
// Refetch workspace data to ensure availableWorkspaces is up to date
void utils.workspace.all.refetch();
router.push(`/boards`);
};
@@ -66,21 +70,15 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
localStorage.getItem("workspacePublicId");
if (data.length) {
const workspaces = data
.map(({ workspace, role }) => {
if (!workspace) return;
return {
role,
publicId: workspace.publicId,
name: workspace.name,
slug: workspace.slug,
description: workspace.description,
plan: workspace.plan,
hasLoaded: true,
};
})
.filter((workspace) => workspace !== null) as Workspace[];
const workspaces = data.map(({ workspace, role }) => ({
role,
publicId: workspace.publicId,
name: workspace.name,
slug: workspace.slug,
description: workspace.description,
plan: workspace.plan,
hasLoaded: true,
})) as Workspace[];
if (workspaces.length) setAvailableWorkspaces(workspaces);
}
@@ -116,7 +114,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
role: primaryWorkspaceRole,
});
}
}, [data]);
}, [data, isLoading]);
return (
<WorkspaceContext.Provider

View File

@@ -18,6 +18,8 @@ const loadMessages = async (locale: Locale) => {
return (await import("~/locales/it/messages")).messages;
case "nl":
return (await import("~/locales/nl/messages")).messages;
case "ru":
return (await import("~/locales/ru/messages")).messages;
default:
return enMessages;
}

View File

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

View File

@@ -19,7 +19,7 @@ export function BoardsList() {
if (isLoading)
return (
<div className="grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-7">
<div className="3xl:grid-cols-4 grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3">
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
@@ -45,7 +45,7 @@ export function BoardsList() {
);
return (
<div className="grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-7">
<div className="3xl:grid-cols-4 grid h-fit w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3">
{data?.map((board) => (
<Link key={board.publicId} href={`boards/${board.publicId}`}>
<div className="align-center relative mr-5 flex h-[150px] w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">

View File

@@ -1,4 +1,5 @@
import { t } from "@lingui/core/macro";
import { useEffect } from "react";
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
import Button from "~/components/Button";
@@ -14,9 +15,13 @@ import { NewBoardForm } from "./components/NewBoardForm";
export default function BoardsPage() {
const { openModal, modalContentType, isOpen } = useModal();
const { workspace, hasLoaded } = useWorkspace();
const { availableWorkspaces, workspace, hasLoaded } = useWorkspace();
if (hasLoaded && !workspace.publicId) openModal("NEW_WORKSPACE");
useEffect(() => {
if (hasLoaded && availableWorkspaces.length === 0) {
openModal("NEW_WORKSPACE");
}
}, [hasLoaded, availableWorkspaces.length, openModal]);
return (
<>

View File

@@ -1,4 +1,3 @@
import { Menu } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { HiMiniPlus } from "react-icons/hi2";
@@ -101,22 +100,19 @@ export default function LabelSelector({
{selectedLabels.length ? (
<div className="flex flex-wrap gap-x-0.5">
{selectedLabels.map((label) => (
<Menu.Button key={label.key}>
<Badge value={label.value} iconLeft={label.leftIcon} />
</Menu.Button>
))}
<Menu.Button>
<Badge
value={t`Add label`}
iconLeft={<HiMiniPlus size={14} />}
key={label.key}
value={label.value}
iconLeft={label.leftIcon}
/>
</Menu.Button>
))}
<Badge value={t`Add label`} iconLeft={<HiMiniPlus size={14} />} />
</div>
) : (
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
<HiMiniPlus size={22} className="pr-2" />
{t`Add label`}
</Menu.Button>
</div>
)}
</CheckboxDropdown>
)}

View File

@@ -1,4 +1,3 @@
import { Menu } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import CheckboxDropdown from "~/components/CheckboxDropdown";
@@ -79,9 +78,9 @@ export default function ListSelector({
}}
asChild
>
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
{selectedList?.value}
</Menu.Button>
</div>
</CheckboxDropdown>
)}
</>

View File

@@ -1,5 +1,4 @@
import { useRouter } from "next/router";
import { Menu } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { HiMiniPlus } from "react-icons/hi2";
@@ -112,11 +111,12 @@ export default function MemberSelector({
createNewItemLabel={t`Invite member`}
asChild
>
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100">
{selectedMembers.length ? (
<div className="isolate flex justify-end -space-x-1 overflow-hidden">
{selectedMembers.map(({ value, imageUrl }) => (
<Avatar
key={value}
size="sm"
name={value}
imageUrl={imageUrl}
@@ -130,7 +130,7 @@ export default function MemberSelector({
{t`Add member`}
</>
)}
</Menu.Button>
</div>
</CheckboxDropdown>
)}
</>

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { Radio, RadioGroup } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { useState } from "react";
import { HiCheckCircle } from "react-icons/hi2";
import { HiBolt, HiCheckCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
type Frequency = "monthly" | "annually";
@@ -94,6 +94,7 @@ const Pricing = () => {
<div className="mt-12 flex flex-col items-center justify-center">
<div className="mb-4 flex items-center gap-2 rounded-full border bg-white px-4 py-1.5 text-center text-xs font-bold text-gray-800 dark:border-dark-300 dark:bg-white dark:text-gray-800 lg:text-sm">
<HiBolt />
<p>
{t`Launch offer: unlimited seats for just $29/month with Pro`}
</p>

View File

@@ -1,6 +1,11 @@
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { HiEllipsisHorizontal, HiOutlinePlusSmall } from "react-icons/hi2";
import {
HiBolt,
HiEllipsisHorizontal,
HiOutlinePlusSmall,
} from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
import type { Subscription } from "@kan/shared/utils";
@@ -124,9 +129,9 @@ export default function MembersPage() {
{memberRole &&
memberRole.charAt(0).toUpperCase() + memberRole.slice(1)}
</span>
{memberStatus === "invited" && (
{(memberStatus === "invited" || memberStatus === "paused") && (
<span className="mt-1 inline-flex items-center rounded-md bg-gray-500/10 px-1.5 py-0.5 text-[10px] font-medium text-gray-400 ring-1 ring-inset ring-gray-500/20 sm:ml-2 sm:mt-0 sm:text-[11px]">
{t`Pending`}
{memberStatus === "invited" ? t`Pending` : t`Paused`}
</span>
)}
</div>
@@ -168,30 +173,45 @@ export default function MembersPage() {
<PageHead title={t`Members | ${workspace.name ?? "Workspace"}`} />
<div className="m-auto h-full max-w-[1100px] p-6 px-5 md:px-28 md:py-12">
<div className="mb-8 flex w-full justify-between">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
{t`Members`}
</h1>
<div className="flex items-center gap-3">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
{t`Members`}
</h1>
</div>
<div className="flex items-center gap-3">
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
<div
className={twMerge(
"flex items-center rounded-full border px-3 py-1 text-center text-xs",
teamSubscription || proSubscription
? "border-emerald-300 bg-emerald-50 text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
: "border-light-300 bg-light-50 text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900",
<>
{!proSubscription && (
<Link
href="/settings/workspace?upgrade=pro"
className="hidden items-center rounded-full border border-emerald-300 bg-emerald-50 px-3 py-1 text-center text-xs text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400 lg:flex"
>
<HiBolt />
<span className="ml-1 font-medium">
{t`Launch offer: Get unlimited members with Pro`}
</span>
</Link>
)}
>
<span className="font-medium">
{proSubscription
? t`Pro Plan`
: teamSubscription
? t`Team Plan`
: t`Free Plan`}
{proSubscription && unlimitedSeats && (
<span className="ml-1 text-xs"></span>
<div
className={twMerge(
"flex items-center rounded-full border px-3 py-1 text-center text-xs",
teamSubscription || proSubscription
? "border-emerald-300 bg-emerald-50 text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
: "border-light-300 bg-light-50 text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900",
)}
</span>
</div>
>
<span className="font-medium">
{proSubscription
? t`Pro Plan`
: teamSubscription
? t`Team Plan`
: t`Free Plan`}
{proSubscription && unlimitedSeats && (
<span className="ml-1 text-xs"></span>
)}
</span>
</div>
</>
)}
<Button
onClick={() => openModal("INVITE_MEMBER")}

View File

@@ -0,0 +1,116 @@
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
import { LanguageSelector } from "~/components/LanguageSelector";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { useModal } from "~/providers/modal";
import { api } from "~/utils/api";
import Avatar from "./components/Avatar";
import { ChangePasswordFormConfirmation } from "./components/ChangePasswordConfirmation";
import { DeleteAccountConfirmation } from "./components/DeleteAccountConfirmation";
import UpdateDisplayNameForm from "./components/UpdateDisplayNameForm";
export default function AccountSettings() {
const { modalContentType, openModal, isOpen } = useModal();
const isCredentialsEnabled =
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
const { data } = api.user.getUser.useQuery();
return (
<>
<PageHead title={t`Settings | Account`} />
<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`Profile picture`}
</h2>
<Avatar userId={data?.id} userImage={data?.image} />
<div className="mb-4">
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
{t`Display name`}
</h2>
<UpdateDisplayNameForm displayName={data?.name ?? ""} />
</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`Language`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Change your language preferences.`}
</p>
<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`Delete account`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Once you delete your account, there is no going back. This action cannot be undone.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("DELETE_ACCOUNT")}
>
{t`Delete account`}
</Button>
</div>
</div>
{isCredentialsEnabled && (
<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`Change Password`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`You are about to change your password.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("CHANGE_PASSWORD")}
>
{t`Change Password`}
</Button>
</div>
</div>
)}
</div>
{/* Account-specific modals */}
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_ACCOUNT"}
>
<DeleteAccountConfirmation />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "CHANGE_PASSWORD"}
>
<ChangePasswordFormConfirmation />
</Modal>
{/* Global modals */}
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "NEW_FEEDBACK"}
>
<FeedbackModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
>
<NewWorkspaceForm />
</Modal>
</>
);
}

View File

@@ -0,0 +1,66 @@
import { t } from "@lingui/core/macro";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { useModal } from "~/providers/modal";
import ApiKeyList from "./components/ApiKeyList";
import NewApiKeyModal from "./components/NewApiKeyModal";
import { RevokeApiKeyConfirmation } from "./components/RevokeApiKeyConfirmation";
export default function ApiSettings() {
const { modalContentType, openModal, isOpen } = useModal();
return (
<>
<PageHead title={t`Settings | API`} />
<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`API keys`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`View and manage your API keys.`}
</p>
<div className="mb-4 flex items-center justify-between">
<Button variant="primary" onClick={() => openModal("NEW_API_KEY")}>
{t`Create new key`}
</Button>
</div>
<ApiKeyList />
</div>
{/* API-specific modals */}
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_API_KEY"}
>
<NewApiKeyModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "REVOKE_API_KEY"}
>
<RevokeApiKeyConfirmation />
</Modal>
{/* Global modals */}
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "NEW_FEEDBACK"}
>
<FeedbackModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
>
<NewWorkspaceForm />
</Modal>
</>
);
}

View File

@@ -0,0 +1,68 @@
import { t } from "@lingui/core/macro";
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { useModal } from "~/providers/modal";
export default function BillingSettings() {
const { modalContentType, isOpen } = useModal();
const handleOpenBillingPortal = async () => {
try {
const response = await fetch("/api/stripe/create_billing_session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const { url } = (await response.json()) as { url: string };
if (url) {
window.location.href = url;
}
} catch (error) {
console.error("Error creating billing session:", error);
}
};
return (
<>
<PageHead title={t`Settings | Billing`} />
<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`Billing`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`View and manage your billing and subscription.`}
</p>
<Button
variant="primary"
iconRight={<HiMiniArrowTopRightOnSquare />}
onClick={handleOpenBillingPortal}
>
{t`Billing portal`}
</Button>
</div>
{/* Global modals */}
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "NEW_FEEDBACK"}
>
<FeedbackModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
>
<NewWorkspaceForm />
</Modal>
</>
);
}

View File

@@ -0,0 +1,130 @@
import { t } from "@lingui/core/macro";
import { useEffect } from "react";
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
export default function IntegrationsSettings() {
const { modalContentType, isOpen } = useModal();
const { showPopup } = usePopup();
const {
data: integrations,
refetch: refetchIntegrations,
isLoading: integrationsLoading,
} = api.integration.providers.useQuery();
const { data: trelloUrl, refetch: refetchTrelloUrl } =
api.integration.getAuthorizationUrl.useQuery(
{ provider: "trello" },
{
enabled:
!integrationsLoading &&
!integrations?.some(
(integration) => integration.provider === "trello",
),
refetchOnWindowFocus: true,
},
);
useEffect(() => {
const handleFocus = () => {
refetchIntegrations();
};
window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("focus", handleFocus);
};
}, [refetchIntegrations]);
const { mutateAsync: disconnectTrello } =
api.integration.disconnect.useMutation({
onSuccess: () => {
refetchIntegrations();
refetchTrelloUrl();
showPopup({
header: t`Trello disconnected`,
message: t`Your Trello account has been disconnected.`,
icon: "success",
});
},
onError: () => {
showPopup({
header: t`Error disconnecting Trello`,
message: t`An error occurred while disconnecting your Trello account.`,
icon: "error",
});
},
});
return (
<>
<PageHead title={t`Settings | Integrations`} />
<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`Trello`}
</h2>
{!integrations?.some(
(integration) => integration.provider === "trello",
) && trelloUrl ? (
<>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Connect your Trello account to import boards.`}
</p>
<Button
variant="primary"
iconRight={<HiMiniArrowTopRightOnSquare />}
onClick={() =>
window.open(
trelloUrl.url,
"trello_auth",
"height=800,width=600",
)
}
>
{t`Connect Trello`}
</Button>
</>
) : (
integrations?.some(
(integration) => integration.provider === "trello",
) && (
<>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Your Trello account is connected.`}
</p>
<Button
variant="secondary"
onClick={() => disconnectTrello({ provider: "trello" })}
>
{t`Disconnect Trello`}
</Button>
</>
)
)}
</div>
{/* Global modals */}
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "NEW_FEEDBACK"}
>
<FeedbackModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
>
<NewWorkspaceForm />
</Modal>
</>
);
}

View File

@@ -0,0 +1,145 @@
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useEffect, useState } from "react";
import { HiBolt } from "react-icons/hi2";
import type { Subscription } from "@kan/shared/utils";
import { hasActiveSubscription } from "@kan/shared/utils";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { useModal } from "~/providers/modal";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
import UpdateWorkspaceDescriptionForm from "./components/UpdateWorkspaceDescriptionForm";
import UpdateWorkspaceNameForm from "./components/UpdateWorkspaceNameForm";
import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm";
import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation";
export default function WorkspaceSettings() {
const { modalContentType, openModal, isOpen } = useModal();
const { workspace } = useWorkspace();
const router = useRouter();
const { data } = api.user.getUser.useQuery();
const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false);
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
const subscriptions = workspaceData?.subscriptions as
| Subscription[]
| undefined;
// Open upgrade modal if upgrade=pro is in URL params
useEffect(() => {
if (
router.query.upgrade === "pro" &&
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
!hasActiveSubscription(subscriptions, "pro") &&
!hasOpenedUpgradeModal
) {
openModal("UPGRADE_TO_PRO");
setHasOpenedUpgradeModal(true);
}
}, [router.query.upgrade, subscriptions, openModal, hasOpenedUpgradeModal]);
return (
<>
<PageHead title={t`Settings | Workspace`} />
<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`Workspace name`}
</h2>
<UpdateWorkspaceNameForm
workspacePublicId={workspace.publicId}
workspaceName={workspace.name}
/>
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
{t`Workspace URL`}
</h2>
<UpdateWorkspaceUrlForm
workspacePublicId={workspace.publicId}
workspaceUrl={workspace.slug ?? ""}
workspacePlan={workspace.plan ?? "free"}
/>
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
{t`Workspace description`}
</h2>
<UpdateWorkspaceDescriptionForm
workspacePublicId={workspace.publicId}
workspaceDescription={workspace.description ?? ""}
/>
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
!hasActiveSubscription(subscriptions, "pro") && (
<div className="my-8">
<Button
onClick={() => openModal("UPGRADE_TO_PRO")}
iconRight={<HiBolt />}
>
{t`Upgrade to Pro`}
</Button>
</div>
)}
<div className="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 workspace`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Once you delete your workspace, there is no going back. This action cannot be undone.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("DELETE_WORKSPACE")}
disabled={workspace.role !== "admin"}
>
{t`Delete workspace`}
</Button>
</div>
</div>
</div>
{/* Workspace-specific modals */}
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_WORKSPACE"}
>
<DeleteWorkspaceConfirmation />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "UPGRADE_TO_PRO"}
>
<UpgradeToProConfirmation
userId={data?.id ?? ""}
workspacePublicId={workspace.publicId}
/>
</Modal>
{/* Global modals */}
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "NEW_FEEDBACK"}
>
<FeedbackModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
>
<NewWorkspaceForm />
</Modal>
</>
);
}

View File

@@ -0,0 +1,202 @@
import { useQuery } from "@tanstack/react-query";
import { HiEllipsisHorizontal } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
import { authClient } from "@kan/auth/client";
import Dropdown from "~/components/Dropdown";
import { useModal } from "~/providers/modal";
export default function ApiKeyList() {
const { openModal } = useModal();
const { data, isLoading } = useQuery({
queryKey: ["apiKeys"],
queryFn: () => authClient.apiKey.list(),
});
const TableRow = ({
keyId,
keyName,
keyStart,
createdAt,
lastRequest,
isLastRow,
showSkeleton,
}: {
keyId?: string;
keyName?: string | null | undefined;
keyStart?: string | null | undefined;
createdAt?: Date | null;
lastRequest?: Date | null;
isLastRow?: boolean | undefined;
showSkeleton?: boolean | undefined;
}) => {
const formatDate = (date?: Date | string | null) => {
if (!date) return "Never";
const dateObj = date instanceof Date ? date : new Date(date);
return dateObj.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
};
return (
<tr className="rounded-b-lg">
<td className={twMerge("w-[30%]", isLastRow ? "rounded-bl-lg" : "")}>
<div className="flex items-center p-4">
<div className="ml-2 min-w-0 flex-1">
<div>
<div className="flex items-center">
<p
className={twMerge(
"mr-2 text-sm font-medium text-light-900 dark:text-dark-900",
showSkeleton &&
"md mb-2 h-3 w-[125px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
)}
>
{keyName}
</p>
</div>
</div>
</div>
</div>
</td>
<td className="w-[20%] px-3 py-4">
<p
className={twMerge(
"text-sm text-light-900 dark:text-dark-900",
showSkeleton &&
"h-3 w-[80px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
)}
>
{formatDate(createdAt)}
</p>
</td>
<td className="w-[20%] px-3 py-4">
<p
className={twMerge(
"text-sm text-light-900 dark:text-dark-900",
showSkeleton &&
"h-3 w-[80px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
)}
>
{formatDate(lastRequest)}
</p>
</td>
<td className="w-[25%] px-3 py-4">
<div>
<span
className={twMerge(
"inline-flex items-center rounded-md bg-emerald-500/10 px-1.5 py-0.5 text-[11px] font-medium text-emerald-400 ring-1 ring-inset ring-emerald-500/20",
showSkeleton &&
"h-5 w-[50px] animate-pulse bg-light-200 ring-0 dark:bg-dark-200",
)}
>
{keyStart}...
</span>
</div>
</td>
<td
className={twMerge(
"w-[5%] min-w-[50px]",
isLastRow && "rounded-br-lg",
)}
>
<div className="flex w-full items-center justify-center px-3">
<div className="relative z-50">
<Dropdown
items={[
{
label: "Revoke",
action: () =>
openModal("REVOKE_API_KEY", keyId, keyName ?? ""),
},
]}
>
<HiEllipsisHorizontal
size={25}
className="text-light-900 dark:text-dark-900"
/>
</Dropdown>
</div>
</div>
</td>
</tr>
);
};
if (!isLoading && (!data?.data || data.data.length === 0)) {
return null;
}
return (
<div className="mt-8 flow-root">
<div className="overflow-x-auto overflow-y-visible">
<div className="inline-block min-w-full py-2 pb-12 align-middle">
<div className="relative h-full shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table className="min-w-[600px] divide-y divide-light-600 dark:divide-dark-600">
<thead className="rounded-t-lg bg-light-300 dark:bg-dark-200">
<tr>
<th
scope="col"
className="w-[30%] rounded-tl-lg py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:pl-6"
>
Name
</th>
<th
scope="col"
className="w-[20%] px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
>
Created
</th>
<th
scope="col"
className="w-[20%] px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
>
Last Used
</th>
<th
scope="col"
className="w-[25%] px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
>
Key
</th>
<th
scope="col"
className="w-[5%] rounded-tr-lg px-3 py-3.5 text-center text-sm font-semibold text-light-900 dark:text-dark-900"
>
{/* Actions column */}
</th>
</tr>
</thead>
<tbody className="divide-y divide-light-600 bg-light-50 dark:divide-dark-600 dark:bg-dark-100">
{!isLoading &&
data?.data?.map((apiKey, index) => (
<TableRow
key={apiKey.id}
keyId={apiKey.id}
keyName={apiKey.name}
keyStart={apiKey.start}
createdAt={apiKey.createdAt}
lastRequest={apiKey.lastRequest}
isLastRow={index === data.data.length - 1}
/>
))}
{isLoading && (
<>
<TableRow showSkeleton />
<TableRow showSkeleton />
<TableRow showSkeleton isLastRow />
</>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,14 +1,45 @@
import Image from "next/image";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useState } from "react";
import { useCallback, useRef, useState } from "react";
import ReactCrop from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
import { generateUID } from "@kan/shared/utils";
import Button from "~/components/Button";
import Modal from "~/components/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import { getAvatarUrl } from "~/utils/helpers";
interface PercentCrop {
unit: "%";
x: number;
y: number;
width: number;
height: number;
}
interface LocalPixelCrop {
x: number;
y: number;
width: number;
height: number;
}
interface ReactCropProps {
crop: PercentCrop | undefined;
onChange: (crop: LocalPixelCrop, percentCrop: PercentCrop) => void;
aspect?: number;
className?: string;
circularCrop?: boolean;
children: React.ReactNode;
}
const AnyReactCrop = ReactCrop as unknown as React.FC<ReactCropProps>;
export default function Avatar({
userId,
userImage,
@@ -19,6 +50,13 @@ export default function Avatar({
const utils = api.useUtils();
const { showPopup } = usePopup();
const [uploading, setUploading] = useState(false);
const [cropDialogOpen, setCropDialogOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedPreviewUrl, setSelectedPreviewUrl] = useState<string | null>(
null,
);
const [crop, setCrop] = useState<PercentCrop>();
const imgRef = useRef<HTMLImageElement | null>(null);
const updateUser = api.user.update.useMutation({
onSuccess: async () => {
@@ -45,24 +83,109 @@ export default function Avatar({
const avatarUrl = userImage ? getAvatarUrl(userImage) : undefined;
const uploadAvatar = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
event.preventDefault();
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const file = event.target.files?.[0] ?? null;
if (!file || !userId) {
return showPopup({
header: t`Error uploading profile image`,
message: t`Please select a file to upload.`,
icon: "error",
});
}
// Open crop dialog with preview
setSelectedFile(file);
const objUrl = URL.createObjectURL(file);
setSelectedPreviewUrl(objUrl);
setCropDialogOpen(true);
};
const file = event.target.files?.[0];
if (!file || !userId) {
return showPopup({
header: t`Error uploading profile image`,
message: t`Please select a file to upload.`,
icon: "error",
});
const onImageLoad = useCallback(
(e: React.SyntheticEvent<HTMLImageElement>) => {
const { naturalWidth, naturalHeight } = e.currentTarget;
// Create a centered square crop at ~90% of the smaller dimension
// Compute width% so that the square fits within the image
let widthPercent: number;
let heightPercent: number;
if (naturalWidth >= naturalHeight) {
// landscape: height is limiting
heightPercent = 90;
widthPercent = (naturalHeight / naturalWidth) * heightPercent;
} else {
// portrait: width is limiting
widthPercent = 90;
heightPercent = (naturalWidth / naturalHeight) * widthPercent;
}
const x = (100 - widthPercent) / 2;
const y = (100 - heightPercent) / 2;
setCrop({ unit: "%", x, y, width: widthPercent, height: heightPercent });
},
[],
);
const fileExt = file.name.split(".").pop();
const fileName = `${userId}/avatar-${generateUID()}.${fileExt}`;
const getCroppedBlob = useCallback(async (): Promise<Blob> => {
if (!imgRef.current || !crop) throw new Error("No crop to save");
const image = imgRef.current;
const canvas = document.createElement("canvas");
const cropXpx = (crop.x / 100) * image.naturalWidth;
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));
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,
cropYpx,
cropWpx,
cropHpx,
0,
0,
canvas.width / pixelRatio,
canvas.height / pixelRatio,
);
const mime = selectedFile?.type ?? "image/jpeg";
const blob: Blob = await new Promise((resolve, reject) => {
canvas.toBlob(
(b) => (b ? resolve(b) : reject(new Error("toBlob failed"))),
mime,
);
});
return blob;
}, [crop, selectedFile]);
const resetCropState = useCallback(() => {
setCrop(undefined);
setSelectedFile(null);
if (selectedPreviewUrl) URL.revokeObjectURL(selectedPreviewUrl);
setSelectedPreviewUrl(null);
}, [selectedPreviewUrl]);
const handleCancelCrop = useCallback(() => {
setCropDialogOpen(false);
resetCropState();
}, [resetCropState]);
const handleSaveCrop = useCallback(async () => {
try {
if (!userId || !selectedFile) return;
setUploading(true);
const blob = await getCroppedBlob();
const originalExt = selectedFile.name.split(".").pop() ?? "jpg";
const fileName = `${userId}/avatar-${generateUID()}.${originalExt}`;
const response = await fetch(
env("NEXT_PUBLIC_BASE_URL") + "/api/upload/image",
@@ -71,26 +194,24 @@ export default function Avatar({
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ filename: fileName, contentType: file.type }),
body: JSON.stringify({ filename: fileName, contentType: blob.type }),
},
);
if (!response.ok) throw new Error("Failed to get pre-signed URL");
const { url } = (await response.json()) as {
url: string;
};
const { url } = (await response.json()) as { url: string };
const uploadResponse = await fetch(url, {
method: "PUT",
body: file,
body: blob,
});
if (!uploadResponse.ok) throw new Error("Failed to upload profile image");
updateUser.mutate({
image: fileName,
});
updateUser.mutate({ image: fileName });
setCropDialogOpen(false);
resetCropState();
} catch (error) {
console.error(error);
showPopup({
@@ -101,7 +222,14 @@ export default function Avatar({
} finally {
setUploading(false);
}
};
}, [
getCroppedBlob,
resetCropState,
selectedFile,
showPopup,
updateUser,
userId,
]);
return (
<div>
@@ -111,7 +239,7 @@ export default function Avatar({
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
onChange={onFileChange}
disabled={uploading}
/>
{avatarUrl ? (
@@ -134,6 +262,56 @@ export default function Avatar({
</span>
)}
</div>
{/* Crop Dialog */}
{cropDialogOpen && (
<Modal modalSize="md" positionFromTop="sm" isVisible>
<div className="p-4 sm:p-6">
<div className="mb-4">
<h3 className="text-base font-semibold text-light-1000 dark:text-dark-1000">
{t`Crop your avatar`}
</h3>
<p className="mt-1 text-sm text-light-800 dark:text-dark-800">
{t`Adjust the square crop to fit your avatar.`}
</p>
</div>
<div className="max-h-[80vh]">
<div className="rounded-md border border-light-600 p-2 dark:border-dark-600">
<AnyReactCrop
crop={crop}
onChange={(_crop: LocalPixelCrop, percentCrop: PercentCrop) =>
setCrop(percentCrop)
}
aspect={1}
circularCrop
className="w-full"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
ref={imgRef}
src={selectedPreviewUrl ?? undefined}
alt="Avatar to crop"
onLoad={onImageLoad}
className="h-auto max-h-[50vh] w-full object-contain"
/>
</AnyReactCrop>
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-2">
<Button
variant="ghost"
onClick={handleCancelCrop}
disabled={uploading}
>
{t`Cancel`}
</Button>
<Button onClick={handleSaveCrop} isLoading={uploading}>
{t`Save`}
</Button>
</div>
</div>
</Modal>
)}
</div>
);
}

View File

@@ -1,60 +0,0 @@
import { t } from "@lingui/core/macro";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import Input from "~/components/Input";
const CreateAPIKeyForm = ({
apiKey,
refetchUser,
}: {
apiKey:
| {
id: number;
prefix: string | null;
key: string;
}
| null
| undefined;
refetchUser: () => void;
}) => {
const handleCreateAPIKey = async () => {
await authClient.apiKey.create({
name: "Kan API Key",
prefix: "kan_",
});
refetchUser();
};
const handleRevokeAPIKey = async () => {
if (!apiKey) return;
await authClient.apiKey.delete({
keyId: apiKey.id.toString(),
});
refetchUser();
};
return (
<div>
{apiKey ? (
<div className="flex gap-2">
<div className="mb-4 flex w-full max-w-[325px] items-center gap-2">
<Input value={apiKey.key} readOnly type="password" />
</div>
<div>
<Button variant="danger" onClick={handleRevokeAPIKey}>
{t`Revoke`}
</Button>
</div>
</div>
) : (
<Button onClick={handleCreateAPIKey}>{t`Create new key`}</Button>
)}
</div>
);
};
export default CreateAPIKeyForm;

View File

@@ -0,0 +1,69 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import Input from "~/components/Input";
import { useModal } from "~/providers/modal";
const newApiKeySchema = z.object({
name: z.string().min(1),
});
export default function NewApiKeyForm() {
const { openModal } = useModal();
const form = useForm({
resolver: zodResolver(newApiKeySchema),
defaultValues: {
name: "",
},
});
const qc = useQueryClient();
const createApiKeyMutation = useMutation({
mutationFn: ({ name }: { name: string }) =>
authClient.apiKey.create({ name, prefix: "kan_" }),
onSuccess: ({ data: apiKey }) => {
qc.invalidateQueries({
queryKey: ["apiKeys"],
});
openModal("API_KEY_CREATED", apiKey?.key, apiKey?.name ?? "");
},
onError: () => {
form.setError("name", {
type: "manual",
message: "Failed to create API key",
});
},
});
const handleSubmit = (data: z.infer<typeof newApiKeySchema>) => {
createApiKeyMutation.mutate({ name: data.name });
};
return (
<div className="px-2 py-2">
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="flex flex-col gap-2"
>
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
New API key
</h2>
<Input
{...form.register("name")}
placeholder="Name"
className="w-full"
errorMessage={form.formState.errors.name?.message}
/>
<Button type="submit" isLoading={createApiKeyMutation.isPending}>
Create
</Button>
</form>
</div>
);
}

View File

@@ -0,0 +1,181 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import {
HiInformationCircle,
HiMiniCheck,
HiOutlineDocumentDuplicate,
HiXMark,
} from "react-icons/hi2";
import { z } from "zod";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import Input from "~/components/Input";
import { useClipboard } from "~/hooks/useClipboard";
import { useModal } from "~/providers/modal";
const newApiKeySchema = z.object({
name: z
.string()
.min(1, { message: t`API key name is required` })
.max(30, { message: t`API key name cannot exceed 30 characters` }),
});
export default function NewApiKeyModal() {
const { closeModal } = useModal();
const { copied, copy } = useClipboard({ timeout: 2000 });
const [createdApiKey, setCreatedApiKey] = useState<{
key: string;
name: string;
} | null>(null);
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<z.infer<typeof newApiKeySchema>>({
resolver: zodResolver(newApiKeySchema),
defaultValues: {
name: "",
},
});
const qc = useQueryClient();
const createApiKeyMutation = useMutation({
mutationFn: ({ name }: { name: string }) =>
authClient.apiKey.create({ name, prefix: "kan_" }),
onSuccess: ({ data: apiKey }) => {
void qc.invalidateQueries({
queryKey: ["apiKeys"],
});
if (apiKey && apiKey.key && apiKey.name) {
setCreatedApiKey({
key: apiKey.key,
name: apiKey.name,
});
}
},
onError: () => {
// Handle error if needed
},
});
const onSubmit = (data: z.infer<typeof newApiKeySchema>) => {
createApiKeyMutation.mutate({ name: data.name });
};
useEffect(() => {
// Reset state and form when modal opens
setCreatedApiKey(null);
reset();
}, [reset]);
useEffect(() => {
if (!createdApiKey) {
const nameElement = document.querySelector<HTMLElement>("#name");
if (nameElement) nameElement.focus();
}
}, [createdApiKey]);
if (createdApiKey) {
return (
<div>
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
<h2 className="text-sm font-bold">{t`API key created`}</h2>
<button
type="button"
className="rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
}}
>
<HiXMark
size={18}
className="text-light-900 dark:text-dark-900"
/>
</button>
</div>
<div className="mb-4">
<div className="relative">
<Input
value={createdApiKey.key}
className="pr-10 text-sm text-light-900 dark:text-dark-900"
readOnly
/>
<button
type="button"
className="absolute inset-y-0 right-0 flex items-center pr-3 text-light-900 hover:text-light-950 dark:text-dark-900 dark:hover:text-dark-950"
onClick={() => copy(createdApiKey.key)}
>
{copied ? (
<HiMiniCheck className="h-5 w-5 text-green-600" />
) : (
<HiOutlineDocumentDuplicate className="h-5 w-5" />
)}
</button>
</div>
<div className="mt-2 flex items-start gap-1">
<HiInformationCircle className="mt-0.5 h-4 w-4 text-dark-900" />
<p className="text-xs text-gray-500 dark:text-dark-900">
{t`This API key will only be shown once. Please save it in a secure location.`}
</p>
</div>
</div>
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<div>
<Button onClick={() => closeModal()}>{t`Close`}</Button>
</div>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
<h2 className="text-sm font-bold">{t`New API key`}</h2>
<button
type="button"
className="rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
}}
>
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button>
</div>
<Input
id="name"
placeholder={t`API key name`}
{...register("name", { required: true })}
errorMessage={errors.name?.message}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
await handleSubmit(onSubmit)();
}
}}
/>
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<div>
<Button type="submit" isLoading={createApiKeyMutation.isPending}>
{t`Create API key`}
</Button>
</div>
</div>
</form>
);
}

View File

@@ -0,0 +1,96 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
export function RevokeApiKeyConfirmation() {
const { closeModal, entityId, entityLabel } = useModal();
const { showPopup } = usePopup();
const qc = useQueryClient();
const [isAcknowledgmentChecked, setIsAcknowledgmentChecked] = useState(false);
const deleteApiKeyMutation = useMutation({
mutationFn: () => authClient.apiKey.delete({ keyId: entityId }),
onSuccess: async () => {
closeModal();
showPopup({
header: "API key revoked",
message: `Your API key: ${entityLabel} has been revoked.`,
icon: "success",
});
qc.invalidateQueries({
queryKey: ["apiKeys"],
});
},
onError: () => {
closeModal();
showPopup({
header: "Error revoking API key",
message: "Please try again later, or contact customer support.",
icon: "error",
});
},
});
const handleRevokeApiKey = () => {
deleteApiKeyMutation.mutate();
};
return (
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
{`Are you sure you want to revoke this API key: ${entityLabel}?`}
</h2>
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
Keep in mind that this action is irreversible.
</p>
<p className="text-sm text-light-900 dark:text-dark-900">
This will result in the permanent revocation of this API key.
</p>
</div>
<div className="relative flex items-start">
<div className="flex h-6 items-center">
<input
id="acknowledgment"
name="acknowledgment"
type="checkbox"
aria-describedby="acknowledgment-description"
className="mt-2 h-[14px] w-[14px] rounded border-gray-300 bg-transparent text-indigo-600 focus:shadow-none focus:ring-0 focus:ring-offset-0"
checked={isAcknowledgmentChecked}
onChange={() =>
setIsAcknowledgmentChecked(!isAcknowledgmentChecked)
}
/>
</div>
<div className="ml-3 text-sm leading-6">
<p
id="comments-description"
className="text-light-900 dark:text-dark-1000"
>
I acknowledge that this API key will be permanently revoked and want
to proceed.
</p>
</div>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button variant="secondary" onClick={() => closeModal()}>
Cancel
</Button>
<Button
variant="danger"
onClick={handleRevokeApiKey}
disabled={!isAcknowledgmentChecked}
isLoading={deleteApiKeyMutation.isPending}
>
Revoke API key
</Button>
</div>
</div>
);
}

View File

@@ -69,16 +69,18 @@ const UpdateDisplayNameForm = ({ displayName }: { displayName: string }) => {
<div className="mb-4 flex w-full max-w-[325px] items-center gap-2">
<Input {...register("name")} errorMessage={errors.name?.message} />
</div>
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={!isDirty || updateDisplayName.isPending}
isLoading={updateDisplayName.isPending}
>
{t`Update`}
</Button>
</div>
{isDirty && (
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={updateDisplayName.isPending}
isLoading={updateDisplayName.isPending}
>
{t`Update`}
</Button>
</div>
)}
</div>
);
};

View File

@@ -80,16 +80,18 @@ const UpdateWorkspaceDescriptionForm = ({
errorMessage={errors.description?.message}
/>
</div>
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={!isDirty || updateWorkspaceDescription.isPending}
isLoading={updateWorkspaceDescription.isPending}
>
{t`Update`}
</Button>
</div>
{isDirty && (
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={updateWorkspaceDescription.isPending}
isLoading={updateWorkspaceDescription.isPending}
>
{t`Update`}
</Button>
</div>
)}
</div>
);
};

View File

@@ -72,16 +72,18 @@ const UpdateWorkspaceNameForm = ({
<div className="mb-4 flex w-full max-w-[325px] items-center gap-2">
<Input {...register("name")} errorMessage={errors.name?.message} />
</div>
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={!isDirty || updateWorkspaceName.isPending}
isLoading={updateWorkspaceName.isPending}
>
{t`Update`}
</Button>
</div>
{isDirty && (
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={updateWorkspaceName.isPending}
isLoading={updateWorkspaceName.isPending}
>
{t`Update`}
</Button>
</div>
)}
</div>
);
};

View File

@@ -138,22 +138,23 @@ const UpdateWorkspaceUrlForm = ({
}
/>
</div>
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={
!isDirty ||
updateWorkspaceSlug.isPending ||
checkWorkspaceSlugAvailability.isPending ||
isWorkspaceSlugAvailable?.isAvailable === false ||
isTyping
}
isLoading={updateWorkspaceSlug.isPending}
>
{t`Update`}
</Button>
</div>
{isDirty && (
<div>
<Button
onClick={handleSubmit(onSubmit)}
variant="primary"
disabled={
updateWorkspaceSlug.isPending ||
checkWorkspaceSlugAvailability.isPending ||
isWorkspaceSlugAvailable?.isAvailable === false ||
isTyping
}
isLoading={updateWorkspaceSlug.isPending}
>
{t`Update`}
</Button>
</div>
)}
</div>
);
};

View File

@@ -76,9 +76,14 @@ export function UpgradeToProConfirmation({
</div>
<div className="flex items-center space-x-3">
<HiCheckBadge className="h-5 w-5 flex-shrink-0 text-light-1000 dark:text-dark-950" />
<span className="text-sm text-neutral-900 dark:text-dark-1000">
{t`Board analytics (coming soon)`}
</span>
<div className="flex items-center space-x-2">
<span className="text-xs text-neutral-900 dark:text-dark-1000">
{t`Board analytics`}
</span>
<span className="inline-flex items-center rounded-full bg-gray-500/10 px-2 py-0.5 text-[10px] font-medium text-gray-600 ring-1 ring-inset ring-gray-500/20 dark:text-gray-400 sm:text-[10px]">
{t`Coming soon`}
</span>
</div>
</div>
<div className="flex items-center space-x-3">
<HiCheckBadge className="h-5 w-5 flex-shrink-0 text-light-1000 dark:text-dark-950" />
@@ -90,7 +95,12 @@ export function UpgradeToProConfirmation({
</div>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button onClick={() => closeModal()} variant="secondary">
<Button
onClick={() => {
closeModal();
}}
variant="secondary"
>
{t`Cancel`}
</Button>
<Button

View File

@@ -1,409 +0,0 @@
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useEffect, useRef } from "react";
import { HiBolt, HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
import type { Subscription } from "@kan/shared/utils";
import { hasActiveSubscription } from "@kan/shared/utils";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
import { LanguageSelector } from "~/components/LanguageSelector";
import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import Avatar from "./components/Avatar";
import { ChangePasswordFormConfirmation } from "./components/ChangePasswordConfirmation";
import CreateAPIKeyForm from "./components/CreateAPIKeyForm";
import { DeleteAccountConfirmation } from "./components/DeleteAccountConfirmation";
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
import UpdateDisplayNameForm from "./components/UpdateDisplayNameForm";
import UpdateWorkspaceDescriptionForm from "./components/UpdateWorkspaceDescriptionForm";
import UpdateWorkspaceNameForm from "./components/UpdateWorkspaceNameForm";
import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm";
import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation";
export default function SettingsPage() {
const { modalContentType, openModal, isOpen } = useModal();
const { workspace } = useWorkspace();
const utils = api.useUtils();
const { showPopup } = usePopup();
const router = useRouter();
const workspaceUrlSectionRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const isCredentialsEnabled =
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
const { data } = api.user.getUser.useQuery();
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
const subscriptions = workspaceData?.subscriptions as
| Subscription[]
| undefined;
const {
data: integrations,
refetch: refetchIntegrations,
isLoading: integrationsLoading,
} = api.integration.providers.useQuery();
const { data: trelloUrl, refetch: refetchTrelloUrl } =
api.integration.getAuthorizationUrl.useQuery(
{ provider: "trello" },
{
enabled:
!integrationsLoading &&
!integrations?.some(
(integration) => integration.provider === "trello",
),
refetchOnWindowFocus: true,
},
);
useEffect(() => {
const handleFocus = () => {
refetchIntegrations();
};
window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("focus", handleFocus);
};
}, [refetchIntegrations]);
useEffect(() => {
if (
router.query.edit === "workspace_url" &&
workspaceUrlSectionRef.current &&
scrollContainerRef.current
) {
const element = workspaceUrlSectionRef.current;
const container = scrollContainerRef.current;
container.scrollTop = element.offsetTop - 40;
const input = element.querySelector('input[type="text"]');
if (input instanceof HTMLInputElement) {
input.focus();
}
}
}, [router.query.edit]);
// Open upgrade modal if upgrade=pro is in URL params
useEffect(() => {
if (
router.query.upgrade === "pro" &&
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
!hasActiveSubscription(subscriptions, "pro")
) {
openModal("UPGRADE_TO_PRO");
}
}, [router.query.upgrade, subscriptions, openModal]);
const { mutateAsync: disconnectTrello } =
api.integration.disconnect.useMutation({
onSuccess: () => {
refetchUser();
refetchIntegrations();
refetchTrelloUrl();
showPopup({
header: t`Trello disconnected`,
message: t`Your Trello account has been disconnected.`,
icon: "success",
});
},
onError: () => {
showPopup({
header: t`Error disconnecting Trello`,
message: t`An error occurred while disconnecting your Trello account.`,
icon: "error",
});
},
});
const refetchUser = () => utils.user.getUser.refetch();
const handleOpenBillingPortal = async () => {
try {
const response = await fetch("/api/stripe/create_billing_session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const { url } = (await response.json()) as { url: string };
if (url) {
window.location.href = url;
}
} catch (error) {
console.error("Error creating billing session:", error);
}
};
return (
<>
<div className="flex h-full w-full flex-col overflow-hidden">
<div
ref={scrollContainerRef}
className="h-full max-h-[calc(100vdh-3rem)] overflow-y-auto md:max-h-[calc(100vdh-4rem)]"
>
<PageHead title={t`Settings | ${workspace.name ?? "Workspace"}`} />
<div className="m-auto max-w-[1100px] px-5 py-6 md:px-28 md:py-12">
<div className="mb-8 flex w-full justify-between">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
{t`Settings`}
</h1>
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Profile picture`}
</h2>
<Avatar userId={data?.id} userImage={data?.image} />
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Display name`}
</h2>
<UpdateDisplayNameForm displayName={data?.name ?? ""} />
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Workspace name`}
</h2>
<UpdateWorkspaceNameForm
workspacePublicId={workspace.publicId}
workspaceName={workspace.name}
/>
<div ref={workspaceUrlSectionRef}>
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Workspace URL`}
</h2>
<UpdateWorkspaceUrlForm
workspacePublicId={workspace.publicId}
workspaceUrl={workspace.slug ?? ""}
workspacePlan={workspace.plan ?? "free"}
/>
</div>
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Workspace description`}
</h2>
<UpdateWorkspaceDescriptionForm
workspacePublicId={workspace.publicId}
workspaceDescription={workspace.description ?? ""}
/>
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
!hasActiveSubscription(subscriptions, "pro") && (
<div className="mt-8">
<Button
onClick={() => openModal("UPGRADE_TO_PRO")}
iconRight={<HiBolt />}
>
{t`Upgrade to Pro`}
</Button>
</div>
)}
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Language`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Change the language of the app.`}
</p>
<LanguageSelector />
</div>
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Billing`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`View and manage your billing and subscription.`}
</p>
<Button
variant="primary"
iconRight={<HiMiniArrowTopRightOnSquare />}
onClick={handleOpenBillingPortal}
>
{t`Billing portal`}
</Button>
</div>
)}
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Trello
</h2>
{!integrations?.some(
(integration) => integration.provider === "trello",
) && trelloUrl ? (
<>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Connect your Trello account to import boards.`}
</p>
<Button
variant="primary"
iconRight={<HiMiniArrowTopRightOnSquare />}
onClick={() =>
window.open(
trelloUrl.url,
"trello_auth",
"height=800,width=600",
)
}
>
{t`Connect Trello`}
</Button>
</>
) : (
integrations?.some(
(integration) => integration.provider === "trello",
) && (
<>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Your Trello account is connected.`}
</p>
<Button
variant="secondary"
onClick={() => disconnectTrello({ provider: "trello" })}
>
{t`Disconnect Trello`}
</Button>
</>
)
)}
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`API keys`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`View and manage your API keys.`}
</p>
<CreateAPIKeyForm
apiKey={data?.apiKey}
refetchUser={refetchUser}
/>
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Delete workspace`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Once you delete your workspace, there is no going back. This action cannot be undone.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("DELETE_WORKSPACE")}
disabled={workspace.role !== "admin"}
>
{t`Delete workspace`}
</Button>
</div>
</div>
{isCredentialsEnabled && (
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Change Password`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`You are about to change your password.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("CHANGE_PASSWORD")}
>
{t`Change Password`}
</Button>
</div>
</div>
)}
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
{t`Delete account`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Once you delete your account, there is no going back. This action cannot be undone.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("DELETE_ACCOUNT")}
>
{t`Delete account`}
</Button>
</div>
</div>
</div>
<>
<Modal
modalSize="md"
isVisible={isOpen && modalContentType === "NEW_FEEDBACK"}
>
<FeedbackModal />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
>
<NewWorkspaceForm />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_WORKSPACE"}
>
<DeleteWorkspaceConfirmation />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "UPGRADE_TO_PRO"}
>
<UpgradeToProConfirmation
userId={data?.id ?? ""}
workspacePublicId={workspace.publicId}
/>
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "DELETE_ACCOUNT"}
>
<DeleteAccountConfirmation />
</Modal>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "CHANGE_PASSWORD"}
>
<ChangePasswordFormConfirmation />
</Modal>
</>
</div>
</div>
</>
);
}

View File

@@ -140,6 +140,12 @@ export const workspaceRouter = createTRPCRouter({
.input(
z.object({
name: z.string().min(1),
slug: z
.string()
.min(3)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
.optional(),
}),
)
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.create>>>())
@@ -153,12 +159,43 @@ export const workspaceRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
// Check if slug is provided in cloud environment
if (input.slug && env("NEXT_PUBLIC_KAN_ENV") === "cloud") {
throw new TRPCError({
message: "Custom URLs are only available for Pro workspaces",
code: "BAD_REQUEST",
});
}
const workspacePublicId = generateUID();
const workspaceSlug = input.slug ?? workspacePublicId;
if (input.slug) {
const reservedOrPremiumWorkspaceSlug =
await workspaceSlugRepo.getWorkspaceSlug(ctx.db, input.slug);
const isWorkspaceSlugAvailable =
await workspaceRepo.isWorkspaceSlugAvailable(ctx.db, input.slug);
if (reservedOrPremiumWorkspaceSlug) {
throw new TRPCError({
message: `Workspace slug '${input.slug}' is reserved or premium`,
code: "BAD_REQUEST",
});
}
if (!isWorkspaceSlugAvailable) {
throw new TRPCError({
message: `Workspace slug '${input.slug}' is already taken`,
code: "BAD_REQUEST",
});
}
}
const result = await workspaceRepo.create(ctx.db, {
publicId: workspacePublicId,
name: input.name,
slug: workspacePublicId,
slug: workspaceSlug,
createdBy: userId,
createdByEmail: userEmail,
});
@@ -334,6 +371,14 @@ export const workspaceRouter = createTRPCRouter({
}),
)
.query(async ({ ctx, input }) => {
const userId = ctx.user?.id;
if (!userId)
throw new TRPCError({
message: `User not authenticated`,
code: "UNAUTHORIZED",
});
const slug = input.workspaceSlug.toLowerCase();
// check slug is not reserved
const workspaceSlug = await workspaceSlugRepo.getWorkspaceSlug(
@@ -345,6 +390,19 @@ export const workspaceRouter = createTRPCRouter({
const isWorkspaceSlugAvailable =
await workspaceRepo.isWorkspaceSlugAvailable(ctx.db, slug);
const isAvailable =
isWorkspaceSlugAvailable && workspaceSlug?.type !== "reserved";
const isReserved = workspaceSlug?.type === "reserved";
if (env("NEXT_PUBLIC_KAN_ENV") === "cloud") {
await workspaceSlugRepo.createWorkspaceSlugCheck(ctx.db, {
slug,
userId,
available: isAvailable,
reserved: isReserved,
});
}
return {
isAvailable:
isWorkspaceSlugAvailable && workspaceSlug?.type !== "reserved",

View File

@@ -220,6 +220,19 @@ export const initAuth = (db: dbClient) => {
console.log(
`Pro subscription ${stripeSubscription.id} activated with unlimited seats`,
);
const workspace = await workspaceRepo.getByPublicId(
db,
subscription.referenceId,
);
if (workspace?.id) {
await memberRepo.unpauseAllMembers(db, workspace.id);
console.log(
`Unpausing all members for workspace ${workspace.id}`,
);
}
}
},
},

View File

@@ -0,0 +1 @@
ALTER TABLE "subscription" ALTER COLUMN "referenceId" DROP NOT NULL;

View File

@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS "workspace_slug_checks" (
"id" bigserial PRIMARY KEY NOT NULL,
"slug" varchar(255) NOT NULL,
"available" boolean NOT NULL,
"reserved" boolean NOT NULL,
"workspaceId" bigint,
"createdBy" uuid,
"createdAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "workspace_slug_checks" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "workspace_slug_checks" ADD CONSTRAINT "workspace_slug_checks_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "workspace_slug_checks" ADD CONSTRAINT "workspace_slug_checks_createdBy_user_id_fk" FOREIGN KEY ("createdBy") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1,7 @@
ALTER TABLE "subscription" DROP CONSTRAINT "subscription_referenceId_workspace_publicId_fk";
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_referenceId_workspace_publicId_fk" FOREIGN KEY ("referenceId") REFERENCES "public"."workspace"("publicId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."member_status" ADD VALUE 'paused';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,34 @@
"when": 1757271312974,
"tag": "20250907185512_AddUnlimitedSeatsToSubscription",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1757533858687,
"tag": "20250910195058_RemoveNotNullConstraintFromReferenceIdOnSubsriptions",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1757534656098,
"tag": "20250910200416_AddWorkspaceSlugChecks",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1757535838766,
"tag": "20250910202358_AddCascadeSetNullToReferenceIdOnSubsriptions",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1758226671081,
"tag": "20250918201751_AddPausedMemberStatus",
"breakpoints": true
}
]
}

View File

@@ -95,3 +95,15 @@ export const softDelete = async (
return result;
};
export const unpauseAllMembers = async (db: dbClient, workspaceId: number) => {
await db
.update(workspaceMembers)
.set({ status: "active" })
.where(
and(
eq(workspaceMembers.workspaceId, workspaceId),
eq(workspaceMembers.status, "paused"),
),
);
};

View File

@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import { slugs } from "@kan/db/schema";
import { slugChecks, slugs } from "@kan/db/schema";
export const getWorkspaceSlug = (db: dbClient, slug: string) => {
return db.query.slugs.findFirst({
@@ -12,3 +12,20 @@ export const getWorkspaceSlug = (db: dbClient, slug: string) => {
where: eq(slugs.slug, slug),
});
};
export const createWorkspaceSlugCheck = (
db: dbClient,
input: {
slug: string;
userId: string;
available: boolean;
reserved: boolean;
},
) => {
return db.insert(slugChecks).values({
slug: input.slug,
available: input.available,
reserved: input.reserved,
createdBy: input.userId,
});
};

View File

@@ -13,9 +13,10 @@ import { workspaces } from "./workspaces";
export const subscription = pgTable("subscription", {
id: bigserial("id", { mode: "number" }).primaryKey(),
plan: varchar("plan", { length: 255 }).notNull(),
referenceId: varchar("referenceId", { length: 12 })
.notNull()
.references(() => workspaces.publicId),
referenceId: varchar("referenceId", { length: 12 }).references(
() => workspaces.publicId,
{ onDelete: "set null" },
),
stripeCustomerId: varchar("stripeCustomerId", { length: 255 }),
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 255 }),
status: varchar("status", { length: 255 }).notNull(),

View File

@@ -2,6 +2,7 @@ import { relations } from "drizzle-orm";
import {
bigint,
bigserial,
boolean,
pgEnum,
pgTable,
text,
@@ -18,7 +19,12 @@ export const memberRoles = ["admin", "member", "guest"] as const;
export type MemberRole = (typeof memberRoles)[number];
export const memberRoleEnum = pgEnum("role", memberRoles);
export const memberStatuses = ["invited", "active", "removed"] as const;
export const memberStatuses = [
"invited",
"active",
"removed",
"paused",
] as const;
export type MemberStatus = (typeof memberStatuses)[number];
export const memberStatusEnum = pgEnum("member_status", memberStatuses);
@@ -103,3 +109,17 @@ export const slugs = pgTable("workspace_slugs", {
slug: varchar("slug", { length: 255 }).notNull().unique(),
type: slugTypeEnum("type").notNull(),
});
export const slugChecks = pgTable("workspace_slug_checks", {
id: bigserial("id", { mode: "number" }).primaryKey(),
slug: varchar("slug", { length: 255 }).notNull(),
available: boolean("available").notNull(),
reserved: boolean("reserved").notNull(),
workspaceId: bigint("workspaceId", { mode: "number" }).references(
() => workspaces.id,
),
createdBy: uuid("createdBy").references(() => users.id, {
onDelete: "set null",
}),
createdAt: timestamp("createdAt").defaultNow().notNull(),
}).enableRLS();

View File

@@ -14,7 +14,7 @@ export interface Subscription {
unlimitedSeats: boolean;
periodStart: Date | null;
periodEnd: Date | null;
referenceId: string;
referenceId: string | null;
stripeSubscriptionId: string | null;
stripeCustomerId: string | null;
createdAt: Date;

12
pnpm-lock.yaml generated
View File

@@ -205,6 +205,9 @@ importers:
react-icons:
specifier: ^5.5.0
version: 5.5.0(react@18.3.1)
react-image-crop:
specifier: ^11.0.10
version: 11.0.10(react@18.3.1)
react-lottie-player:
specifier: ^1.5.5
version: 1.5.6(react@18.3.1)
@@ -6116,6 +6119,11 @@ packages:
peerDependencies:
react: '*'
react-image-crop@11.0.10:
resolution: {integrity: sha512-+5FfDXUgYLLqBh1Y/uQhIycpHCbXkI50a+nbfkB1C0xXXUTwkisHDo2QCB1SQJyHCqIuia4FeyReqXuMDKWQTQ==}
peerDependencies:
react: '>=16.13.1'
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -13696,6 +13704,10 @@ snapshots:
dependencies:
react: 18.3.1
react-image-crop@11.0.10(react@18.3.1):
dependencies:
react: 18.3.1
react-is@16.13.1: {}
react-is@17.0.2: {}