Compare commits

..
125 Commits
Author SHA1 Message Date
NotAlexNoyle 6e4c0539af Exclude CODEOWNERS from language statistics 2026-10-31 16:52:22 -04:00
NotAlexNoyle b8fcef2fd4 Add CODEOWNERS (one staff approval) 2026-10-31 16:52:21 -04:00
NotAlexNoyle 4e2e6adc73 Fix card details import 2026-06-13 11:54:24 -04:00
NotAlexNoyle 091541a960 Hide minicard completion checkbox 2026-06-13 11:52:03 -04:00
github-actions[bot] 967b8482d3 Bump versions for v9.42 2026-06-13 14:38:34 +00:00
Lauri Ojansivu cb43837c32 Updated ChangeLog. 2026-06-13 17:31:44 +03:00
Lauri Ojansivu 899617bd4d Greatly improved import from Trello to WeKan.
- Attachments now import. Trello attachment URLs require OAuth to download, so
  WeKan could not fetch them. The Trello import page now has two separate file
    fields: a **.json** field for a single Trello board, and a
    **.zip** field for a package produced by the
    [Trello Card Attachments Downloader](https://github.com/wekan/trello-attachments-downloader).
    The .zip may contain one *or more* board .json files together with each
    board's attachments in a board-name subfolder. The .zip is uploaded over HTTP
    and processed entirely on the server (the attachment bytes never travel over
    the realtime WebSocket, which previously overflowed the connection and made
    the page flicker): WeKan detects every board .json, matches each board to its
    attachment subfolder by name, streams each file straight into the Default
    file storage, imports all boards, then opens All Boards. The upload is guarded
    against abuse — it rejects oversized uploads, zip bombs (too many files or too
    much uncompressed data, per-file size caps) and unsafe entry paths (absolute
    paths or `..` traversal), and never overwrites existing files (stored under
    generated ids). The importer also reads attachments from `card.attachments`
    (newer Trello exports), not only from the action log, and uses the correct
    Meteor-Files insert API (the previous calls were no-ops on Meteor 3). Single
    board imports (the .json file or the pasted JSON, after the map-members step)
    now run through the same server-side HTTP path rather than the realtime
    `importBoard` method, so a heavy import can no longer drop the WebSocket and
    get retried in an endless loop (the page flickering with "Invalid frame
    header" / repeated "logged out" errors). When importing only a single board
    .json and you have saved Trello API key and token, WeKan uses them on the
    server to download that board's attachments, background image, member
    avatars and card stickers from Trello (a .json export often omits stickers;
    avatars only fill in mapped users who have none — an existing avatar is never
    overwritten); each download is best-effort and a failure never aborts the
    import. After a single-board import the new board's data is subscribed and
    loaded before opening it, so it shows its swimlanes, lists and cards
    immediately instead of needing a browser page reload (the HTTP import, unlike
    the old realtime method, does not push the written documents to the client by
    itself).
  - Import Trello-only card features: stickers (shown as Font Awesome icons on
    minicards and the card detail), card cover (color or attachment), card
    location (name, address, coordinates with an OpenStreetMap link), and the
    due-date "complete" checkbox shown on the minicard. Trello's two named
    sticker packs are renamed and highlighted (the icon keeps its normal colour)
    so they stay distinct: the "taco" pack becomes "mascot" with an underlined
    icon, and the "pete" pack becomes "computer" with a ring around the icon,
    e.g. "pete-ghost" imports as "computer ghost" with a Snapchat-ghost icon.
    Several named stickers that used to share an icon now get their own Font
    Awesome 4.7 icon (mascot active => heartbeat, pixel => qrcode, proto =>
    flask, embarrassed => meh-o, clean => bath, computer shipped => truck).
  - Live Trello API import: paste your Trello API key and token on the Trello
    import page to list all your Trello workspaces and boards, then import all
    or selected boards together with their attachments (downloaded
    server-side). Imported boards are placed under a personal workspace named
    after their Trello workspace, created by name if it does not exist, under an
    optional parent workspace you choose. The API client respects Trello's rate
    limits: it spaces out requests, honours `Retry-After` on HTTP 429, retries
    transient 5xx and network errors with capped exponential backoff, and
    reports a clear error on an invalid key/token (401). After a successful API
    import you are taken to the All Boards page where the new boards appear.
  - The live API import runs as a background job on the server: you can navigate
    away and come back to the Trello import page to watch progress, see
    per-board results, and read the full error log. Errors are shown in a
    selectable text box with a one-click "Copy to clipboard" button (and you can
    still select and copy just part of the text) so they are easy to share when
    fixing issues. If the import stops on a fatal API error (invalid token, or
    rate limit still failing after retries) you can fix the cause and Resume
    from where it left off, and a server restart leaves the job paused for
    resuming rather than lost. You can also Cancel the import, or Cancel and
    delete the boards already imported, to start the whole process over cleanly.
  - You can optionally save your Trello API key and token with the Save and
    Delete buttons on the import page. When saved, they are stored only on the
    server, never sent back to the browser, and are reused automatically when
    you list workspaces or import (so you do not have to paste them again); a
    "saved" indicator is shown, Save can overwrite them with new values, and
    Delete clears them from the database. If you do not save them, they are kept
    only in server memory for the running import and re-supplied when you resume.
  - Larger Trello JSON exports can be selected as a file instead of pasted.
  - Keep imported usernames for later mapping. Imported members that match an
    existing WeKan user (by username, or by a previously recorded imported
    username) are mapped automatically, and the imported username is remembered
    on that user so future imports map too. Members with no matching WeKan user
    are no longer lost: their usernames are kept on the board, so an admin can
    map them to real users later in Admin Panel / People (Imported Usernames),
    without having to map everyone up front at import time.
  Thanks to xet7 and Claude.
- Board background images can now be stored in WeKan. A new `backgrounds` file
  storage directory is created alongside `attachments` and `avatars` (using the
  current default storage backend), and Board menu → Board backgrounds lets a
  board admin upload background images, set one as the active board background,
  download, and delete them. Board export now includes the board's background
  images and re-imports them, and a Trello board's background image is
  downloaded and stored on import (so it keeps working even if the original
  Trello URL later changes). Thanks to xet7 and Claude.
- Stickers can be added to and removed from cards directly in WeKan, chosen from
  a set of Font Awesome icons similar to Trello's stickers (previously stickers
  only arrived via Trello import). The picker also includes every "mascot"
  (underlined) and "computer" (ringed) highlighted sticker that a Trello import
  can produce — generated from the same icon mapping the importer uses — so any
  imported sticker can also be added by hand. Stickers show on the minicard and
  in the card detail. Thanks to xet7 and Claude.
- The Trello-style "complete" checkbox (mark a card complete/incomplete,
  independent of the due date) is shown as an animated green checkbox to the
  left of the card title, both on the minicard and in the opened card, with
  "Mark as complete" / "Mark as incomplete" tooltips. It uses the same animated
  checkbox style as Admin Panel / Settings / Announcements and is vertically
  centered with the title text, and the two stay in sync. The minicard and the
  opened-card checkboxes use the same checked style (the board theme's colour, or
  green when no theme is set), so they always look identical. Subtask checkboxes
  and the card-detail custom-field checkbox now use the same animated checkbox as
  checklist items (they previously used static square icons), so every checkbox
  on a card animates consistently. Thanks to xet7 and Claude.
- Cards can now have multiple locations, similar to multiple members. The card
  detail shows a Location section (after Labels and Stickers) listing each
  location with its name, address and an OpenStreetMap link, with an "Add
  location" button to add more and a button to edit or remove each one. A single
  location imported from Trello keeps working and is shown in the same list. The
  Add/Edit location popup can also detect a location from any map link (Google
  Maps, OpenStreetMap, Bing Maps, Apple Maps, or generic `?q=`/`?ll=` links):
  paste the link, press "Detect", and the latitude, longitude and (when present)
  the place name/address are filled in automatically. The popup also has an
  "Open map links at" setting (OpenStreetMap by default, or Google/Bing/Apple
  Maps) saved to the user profile, controlling which map service the location
  "Open in map" links use. Thanks to xet7 and Claude.
- The opened card now docks to the top of the window, overlaying the global and
  board header bars, instead of opening downward from the clicked minicard, and
  it can be dragged all the way to the top without its top hiding behind those
  bars. (At 100% zoom the board wrapper no longer sets a `transform: scale(1)`,
  which had made it the containing block for the fixed card and trapped it below
  the headers.) Thanks to xet7 and Claude.

Fixes #5727,
fixes #2457,
fixes #5818,
fixes #755
2026-06-13 17:19:51 +03:00
Lauri Ojansivu 2757bfb365 Updated translations. 2026-06-13 17:18:06 +03:00
Lauri Ojansivu 1ed942bb8a Updated translations. 2026-06-13 09:09:57 +03:00
Lauri Ojansivu 179157ccd2 Updated translations. 2026-06-13 07:45:56 +03:00
Lauri Ojansivu 3e36b931a6 Updated translations. 2026-06-13 06:57:06 +03:00
Lauri Ojansivu c9e60847d0 Updated translations. 2026-06-13 06:35:09 +03:00
Lauri Ojansivu 4d83dc0c05 Updated translations. 2026-06-13 05:45:53 +03:00
Lauri Ojansivu 995965d157 Updated translations. 2026-06-13 03:01:17 +03:00
Lauri Ojansivu 15fbb314b0 Updated ChangeLog. 2026-06-12 20:21:35 +03:00
Lauri Ojansivu 68e0032c6d Fix Board Export/Import error.
Fix import/clone of inconsistent board JSON so the newest WeKan can import
board exports from any newer or older WeKan version. A syntactically valid
export can still be internally inconsistent (a board member whose user
account was deleted is no longer in the export's `users`, cards pointing at a
missing `listId`, orphaned checklists, or a missing `lists` array). Such
exports previously failed import as "error-json-malformed" or silently
dropped data (cards with an undefined list never rendered). The importer now
skips dangling user references in the member mapper (client
`wekanMembersMapper`, server `wekanmapper` used by board cloning, and
`WekanCreator`), falls back to the first imported list — creating one default
list when the export has none — for cards with a dangling `listId`, and skips
orphaned checklists whose card is missing. The importer remains
version-agnostic: it never reads `_format`, normalizes old `id` vs `_id`
fields, and ignores the source `boardId`.
Added `tests/wekanCreator.inconsistent.test.js` covering these cases.

Thanks to titver968, xet7 and Claude !

Fixes #6371
2026-06-12 20:15:56 +03:00
Lauri Ojansivu cffca535b5 Updated ChangeLog. 2026-06-12 19:49:13 +03:00
Lauri Ojansivu b2d2632e3b Fix Wrong card number after Import.
Thanks to titver968, xet7 and Claude !

Fixes #6372
2026-06-12 19:49:13 +03:00
Lauri Ojansivu 119379d4b4 Updated translations. 2026-06-12 19:49:13 +03:00
github-actions[bot] c434f83dfc Bump versions for v9.41 2026-06-11 22:17:23 +00:00
Lauri Ojansivu 1527f364b1 Updated ChangeLog. 2026-06-12 01:13:32 +03:00
Lauri Ojansivu 5779d94d78 Updated translations. 2026-06-12 01:12:38 +03:00
Lauri Ojansivu 096a3dc46b Add pagination and search to Admin Panel / Reports. Fix pagination at Admin Panel / Organizations and Teams.
Thanks to xet7 and Claude !
2026-06-12 01:12:38 +03:00
github-actions[bot] c883f31d35 Bump versions for v9.40 2026-06-11 18:35:38 +00:00
Lauri Ojansivu 1fa9714d4e Updated ChangeLog. 2026-06-11 21:30:00 +03:00
Lauri Ojansivu 53b64ee222 Fix releasing Helm Charts.
Thanks to xet7 and Claude !
2026-06-11 21:30:00 +03:00
github-actions[bot] e4b8396a3b Bump versions for v9.39 2026-06-11 18:19:21 +00:00
Lauri Ojansivu 8cf7e83493 Updated ChangeLog. 2026-06-11 21:15:15 +03:00
Lauri Ojansivu 68220f944b Fix building Docker image.
Thanks to xet7 and Claude !
2026-06-11 21:12:31 +03:00
github-actions[bot] fc4d84574e Bump versions for v9.38 2026-06-11 17:52:20 +00:00
Lauri Ojansivu 2989e1d5b1 Prepare v9.38 release 2026-06-11 20:47:00 +03:00
Lauri Ojansivu 5844e20d55 Fix Incorrect link in invite email.
Thanks to sbaecker, xet7 and Claude !

Fixes #6349
2026-06-11 20:44:25 +03:00
Lauri Ojansivu 2376237b6e Updated ChangeLog. 2026-06-11 20:23:48 +03:00
Lauri Ojansivu bcf55bdc3a Fix Admin Panel/People View is broken.
Thanks to titver968, xet7 and Claude !

Fixes #6370
2026-06-11 20:17:51 +03:00
Lauri Ojansivu ac44706461 Updated ChangeLog. 2026-06-11 20:04:04 +03:00
Lauri Ojansivu 928db8d675 Fix Docker image is broken.
Thanks to titver968, planet-goldfish, xet7 and Claude !

Fixes #6369
2026-06-11 20:04:04 +03:00
github-actions[bot] 5c9407f729 Bump versions for v9.37 2026-06-11 14:34:36 +00:00
Lauri Ojansivu 93f50a7fb3 Prepare v9.37 release 2026-06-11 17:27:45 +03:00
Lauri Ojansivu 39d84f8fe8 Update release website script version numbering.
Thanks to xet7 and Claude !
2026-06-11 17:25:12 +03:00
Lauri Ojansivu d369a3614a [Fixed BoardBleed: broken access control lets any authenticated user move their Cards/Lists/Swimlanes into a private board they are not a member of (cross-board write via collection allow rule)](https://github.com/wekan/wekan/security/advisories/GHSA-gm7v-pc38-53jr)
(CWE-284, CWE-639). WeKan boards are membership-scoped, but the DDP collection write
policies for Cards, Lists and Swimlanes (`server/permissions/cards.js`,
`server/permissions/lists.js`, `server/permissions/swimlanes.js`) authorized an update by
checking only the CURRENT (pre-update) `boardId` of the document — i.e. the attacker's own
source board — and never validated the NEW `boardId` supplied in the update modifier.
Because every logged-in user can create a board where they are admin, an attacker could take
a document they own and, in a single `/cards/update`, `/lists/update` or `/swimlanes/update`
DDP call, `$set` its `boardId` (plus `swimlaneId`/`listId`) to a victim's private board: the
allow rule saw the attacker's own source board, approved the write, and the document was
relocated into a board the attacker is not a member of and cannot even read. This let an
unprivileged user inject arbitrary cards/lists/swimlanes (attacker-controlled titles,
descriptions, assignees, etc.) into any private board by id, defeating board-level access
control. The REST API for the same operation
(`PUT /api/boards/:boardId/lists/:listId/cards/:cardId` with `newBoardId`) was not affected
because it correctly calls `Authentication.checkBoardWriteAccess(req.userId, newBoardId)` on
the destination board; only the DDP allow/deny layer was vulnerable. Fixed by adding a
`denyCrossBoardMove` helper in `server/lib/utils.js` and a `Cards.deny`/`Lists.deny`/
`Swimlanes.deny` `update` rule on each collection that rejects any update whose modifier
`$set`s a `boardId` on which the caller does not have write access, so a cross-board move is
only allowed into a destination board where the user is an active write-capable member.
Affected Wekan v9.35 and earlier.

Thanks to 0xzap, xet7 and Claude.
2026-06-11 17:13:06 +03:00
Lauri Ojansivu 078ab4125e Release scripts: Fix Launchpad build Snap.
Thanks to xet7 and Claude !
2026-06-11 06:34:54 +03:00
github-actions[bot] 3f0dc8b7be Bump versions for v9.36 2026-06-11 02:25:57 +00:00
Lauri Ojansivu c020e09aff Release scripts: Removed build-mac-amd64 that does work.
Thanks to xet7 and Claude !
2026-06-11 05:15:37 +03:00
Lauri Ojansivu b0c007d44f Updated GitHub workflows.
Thanks to xet7 and Claude !
2026-06-11 01:50:08 +03:00
github-actions[bot] b777ee2a12 Bump versions for v9.36 2026-06-10 21:19:30 +00:00
Lauri Ojansivu b328cae552 Prepare v9.36 release 2026-06-11 00:10:49 +03:00
Lauri Ojansivu 5357bb47c3 Updated release scripts.
Thanks to xet7 and Claude !
2026-06-10 23:49:17 +03:00
Lauri Ojansivu 447877733b Updated dependencies.
Thanks to developers of dependencies !
2026-06-10 22:23:27 +03:00
Lauri Ojansivu feb491f8d4 Updated ChangeLog. 2026-06-10 21:48:16 +03:00
Lauri Ojansivu 8f59d76a4e Update release scripts, trying to fix GitHub Actions builds.
Thanks to xet7 and Claude !
2026-06-10 21:46:00 +03:00
Lauri Ojansivu 3a52565ab4 Updated ChangeLog. 2026-06-10 19:42:32 +03:00
Lauri Ojansivu 08ae61161c Fixed TokenBleed: unauthenticated login-token minting via un-awaited auth check in
`POST /api/createtoken/:userId`](https://github.com/wekan/wekan/blob/main/CHANGELOG.md)
(CWE-863, CWE-287). `Authentication.checkUserId` in `server/authentication.js` is an
`async` function, so its 401 (undefined `userId`) and 403 (not an admin) throws become
rejected promises rather than synchronous exceptions. The REST handlers in
`server/models/users.js` and `server/models/boards.js` called it without `await` inside a
plain synchronous `try/catch`, which cannot catch a rejected promise, so the failed check
never stopped execution. `POST /api/createtoken/:userId` then went on to mint and return a
usable login token for any user ID in the URL — including an admin — with no credentials at
all (unauthenticated account takeover). The same detached-rejection bypass also affected
`GET /api/users`, `GET /api/users/:userId`, `PUT /api/users/:userId`, `POST /api/users/`,
`DELETE /api/users/:userId`, `POST /api/deletetoken`, `GET /api/boards`,
`GET /api/boards_count`, `DELETE /api/boards/:boardId`, `GET /api/users/:userId/boards` and
`POST /api/boards/:boardId/copy`. Fixed by awaiting every async `Authentication` check (and
making the two non-`async` handlers `async`) so a failed check rejects before any privileged
code runs. The same un-awaited pattern in the board/card/Excel/PDF export handlers
(`models/export.js`, `models/exportExcel.js`, `models/exportExcelCard.js`, `models/exportPDF.js`,
which were backstopped by `exporter.canExport()`) and in the checklist-create handler
(`server/models/checklists.js`) was given the same await pass. Affected Wekan v9.35 and earlier.

Thanks to Zion Boggan, xet7 and Claude.
2026-06-10 19:39:05 +03:00
Lauri Ojansivu 1835245943 Updated translations 2026-06-10 18:37:30 +03:00
Lauri Ojansivu 83de473e4e Updated ChangeLog. 2026-06-10 18:32:03 +03:00
Lauri Ojansivu a532ee7f14 Fix tests.
Thanks to xet7 and Claude !
2026-06-10 18:29:58 +03:00
Lauri Ojansivu f00b265b34 Updated ChangeLog. 2026-06-10 14:35:12 +03:00
Lauri Ojansivu 2931cc990f Add more API and fix API.
Thanks to xet7 and Claude !

Fixes #5998,
fixes #5897,
fixes #5846,
fixes #5819,
fixes #5813,
fixes #4815,
fixes #4811,
fixes #3062,
fixes #4743
2026-06-10 14:33:15 +03:00
Lauri Ojansivu 25a4672d6d Update build scripts.
Thanks to xet7 !
2026-06-10 11:57:04 +03:00
Lauri Ojansivu 969a4016ee Update rebuild-wekan.sh Node.js version 2026-06-10 11:25:06 +03:00
Lauri Ojansivu 5c2d7079ce Updated ChangeLog. 2026-06-08 22:06:29 +03:00
Lauri Ojansivu d1c71d0526 Updated Code of Conduct.
Thanks to xet7 !
2026-06-08 22:04:32 +03:00
Lauri Ojansivu 1fa2ab0146 Updated ChangeLog. 2026-06-08 21:24:52 +03:00
Lauri Ojansivu 26a03f4845 Improved CONTRIBUTING.md .
Thanks to xet7 !
2026-06-08 21:23:22 +03:00
Lauri Ojansivu 58bfad3adf Updated ChangeLog. 2026-06-08 20:46:29 +03:00
Lauri Ojansivu f1c2f1f40f Improved Security Advisory process.
Thanks to xet7 !
2026-06-08 20:43:54 +03:00
Lauri Ojansivu 7d3e8eb89f Updated ChangeLog. 2026-06-07 01:00:36 +03:00
Lauri Ojansivu 3fe8f602c8 Fix LDAP_SYNC_ADMIN_GROUPS so that admin status sync and group/role sync no longer require LDAP_GROUP_FILTER_ENABLE=true,
which only controls the login restriction filter.

Thanks to titver968, xet7 and Claude.

Fixes #6352
2026-06-07 00:58:46 +03:00
Lauri Ojansivu d87eed36a0 Updated ChangeLog. 2026-06-07 00:48:16 +03:00
Lauri Ojansivu 9cc0660410 Fix MultiSelect, so that it's possible to click select checkbox or minicard to select.
Thanks to xet7 and Claude !
2026-06-07 00:45:56 +03:00
Lauri Ojansivu 58ff88890d Updated ChangeLog. 2026-06-06 23:07:26 +03:00
Lauri Ojansivu 77d60b21a1 Fix Admin Panel / Reports icons and spacing.
Thanks to xet7 !
2026-06-06 23:05:12 +03:00
Lauri Ojansivu 9df2db0314 Updated translations. 2026-06-06 22:51:30 +03:00
Lauri Ojansivu 91839cf3ef Updated translations. 2026-06-06 22:39:05 +03:00
Lauri Ojansivu 74f2352387 v9.35 2026-06-06 10:54:38 +03:00
Lauri Ojansivu f299ca1285 Updated translations. 2026-06-06 10:29:24 +03:00
Lauri Ojansivu f47e67b5b4 Fixed Move Attachments and Avatars.
Thanks to xet7 and Claude !

Fixes #6364
2026-06-06 10:19:03 +03:00
Lauri Ojansivu fe16b94c1e Updated ChangeLog. 2026-06-06 09:59:16 +03:00
Lauri Ojansivu b77b62bfa2 Ask to install npm dependencies before running tests.
Thanks to xet7 and Claude !
2026-06-06 09:57:06 +03:00
Lauri Ojansivu 43d8bf1b92 Updated ChangeLog. 2026-06-06 09:31:39 +03:00
Lauri Ojansivu dca9427bfb New features:
- **Admin Panel / Attachments: the MongoDB GridFS page notes how to make legacy
  files visible.** A translatable line under the "MongoDB GridFS Storage" title
  reminds admins: "To have attachments and avatars visible, move them from
  CollectionFS to any other Storage." Thanks to Claude.
- **Admin Panel / Attachments: the Google Cloud Storage page now documents the
  required bucket permission.** A note under the "GCS Storage" title explains that
  the service account needs the **Storage Object Admin** role
  (`roles/storage.objectAdmin`) on the bucket (Cloud Console → Cloud Storage →
  Buckets → your bucket → Permissions → Grant access → add the service account →
  role "Storage Object Admin"), and that the bucket must exist in the same
  project — otherwise "Test connection" fails with `storage.objects.list denied`.
  Thanks to Claude.
- **Admin Panel / Attachments: a "Save" button next to the Enabled / Read toggles
  at the top of each cloud storage.** S3, Azure and Google Cloud Storage now have
  a Save button directly below the "[ ] Enabled  [ ] Read" row, so those toggles
  can be saved without scrolling to the bottom of the (now quite long) storage
  page. It saves the same way as the existing bottom Save button — the server
  merges that provider's config and preserves secrets left blank — and the bottom
  Save button is kept. (Filesystem and GridFS have only a Read toggle, which
  already saves the instant it is toggled, so they need no button.) Thanks to Claude.

Fixes:

- **Admin Panel / Attachments: "Run MongoDB compact" now actually compacts the
  database.** MongoDB refuses `compact` on an active replica-set primary unless
  `force: true` is given, so every collection failed with "will not run compact
  on an active replica set primary … use force:true to force" — and because the
  usual Meteor setup is a single-node replica set (only a primary, no
  secondaries), nothing was compacted at all and no disk space was reclaimed. The
  primary is now compacted with `force: true` (secondaries are still compacted
  without it, so the primary stays available while they run), so the GridFS
  collections are actually rewritten and freed space is returned to the
  filesystem. Thanks to Claude.
- **Admin Panel / Attachments: clearer cloud "Test connection" errors, and cloud
  config inputs are trimmed.** Testing an Azure/GCS/S3 connection used to report a
  single generic `Incomplete configuration or adapter not installed` even when the
  real problem was specific — e.g. Azure rejecting the config with `Invalid URL`
  (a stray space/newline in the account name, or a bad endpoint / connection
  string). `testCloudConnection` now reports the actual cause: adapter not
  installed, required fields missing, the adapter's own configuration error (such
  as `Invalid URL`), or the real `listFiles` error (auth failure, container not
  found, …). It also pre-validates the config and gives an actionable message
  before the adapter turns it into a cryptic error — e.g. for Azure it explains
  that the **Storage account name** must be just the account name (3–24 lowercase
  letters/numbers, e.g. `wekanstorage`, not a full `https://…` URL and with no
  spaces), or that the **Connection string** is malformed; and when Azure still
  returns `Invalid URL` the message appends which field to check. In addition, all
  cloud-storage text fields are now trimmed when read from the form, so
  leading/trailing whitespace from copy-paste no longer produces an invalid URL.

Thanks to xet7 and Claude.
2026-06-06 09:28:05 +03:00
Lauri Ojansivu 7f17616b75 Updated translations. 2026-06-06 09:23:15 +03:00
Lauri Ojansivu 4a9dcdd0ef Updated translations. 2026-06-06 08:41:23 +03:00
Lauri Ojansivu a1a4176ffb Updated ChangeLog. 2026-06-06 07:30:45 +03:00
Lauri Ojansivu a86a087915 New features:
- **Admin Panel / Attachments: "Calculate file counts" on every storage backend,
  shown right below the "Read" toggle.** Azure Blob Storage and Google Cloud
  Storage now have a "Calculate file counts" button (new `getAzureStorageStats` /
  `getGcsStorageStats` methods) that reports how many attachments and avatars are
  stored on that backend, matching what Filesystem, MongoDB GridFS and S3 already
  offer. The count is read from the stored file metadata (fast, no cloud API
  calls). The S3 button was also moved up to sit directly under its "Read"
  checkbox, so all backends are consistent. Thanks to Claude.
- **Admin Panel / Attachments: bilingual, self-documenting cloud-storage fields
  (S3, Azure and Google Cloud Storage).** Each cloud-storage setting now guides
  the admin from top to bottom with both English and translated text, so it is
  easy to follow along while clicking through the provider's Cloud Console:
  - the field **label** uses the provider's own console wording in English
    (e.g. *Access key ID*, *Secret access key*, *Storage account key*,
    *Service account key (JSON)*), with the translated label shown below it;
  - a literal **example** of what the value should look like
    (e.g. `eu-west-1`, an example bucket name / endpoint / connection string),
    which is intentionally not translated;
  - a short **description** of the value, in English and then translated;
  - the **Cloud Console menu path** showing exactly where to find or create the
    value (e.g. *AWS Console → IAM → Users → your user → Security credentials →
    Access keys → Create access key*; *Azure Portal → Storage accounts → your
    account → Access keys → key1*; *Google Cloud Console → IAM & Admin →
    Service accounts → … → Keys → Add key → Create new key → JSON*), again in
    English and then translated.
  The provider field names, examples and menu paths are kept in English so they
  match what the Cloud Console actually shows, while the descriptions and menu
  paths are also translatable; section titles, the enable/read/force-path-style
  checkboxes and the Test/Save buttons remain fully translated. Thanks to Claude.
- **Admin Panel / Attachments / Move Attachment: "Repair file locations" button
  and a persistent "last move" message.** The new **Repair file locations** button
  scans all attachments and avatars and finds any whose recorded storage
  (`versions.<v>.storage` / `path` / `meta.gridFsFileId`) no longer matches where
  the binary actually is — left inconsistent by an interrupted or failed move —
  and fixes the database to point at the real location: it detects the binary in
  GridFS (by the `metadata.fileId` stamped on upload, recovering files whose
  GridFS id reference was lost) or on the filesystem (using the storage
  strategy's own thorough path resolution, so the repair agrees with what a real
  download/move would find even when `versions.<v>.path` is stale), then corrects
  `storage`, `path` and `meta.gridFsFileId` accordingly. Cloud-stored files are
  left untouched, and files found nowhere are reported as "Not found". The scan is
  streamed (memory-safe) and shows a per-scope searched / repaired / not-found
  summary. Separately, after a move finishes the page now keeps showing
  the last move operation — source → destination (scope) and the date/time as
  `YYYY-MM-DD HH:MM:SS` — so it is clear what was last done. Thanks to Claude.

 New fixes:

- **Fixed moving attachments to S3 (and other cloud storage) failing and
  crashing the server.** Uploading to S3 failed two ways, each of which crashed
  the whole server because the rejection was unhandled and SyncedCron treats
  those as fatal. The `@tweedegolf/sab-adapter-amazon-s3` adapter uploaded with a
  `PutObjectCommand` whose `Body` was a live stream: (1) with no `ContentLength`
  the AWS SDK fell back to a chunked signed upload that needs a decoded content
  length, which was undefined for a stream of unknown size — `Invalid value
  "undefined" for header "x-amz-decoded-content-length"`; and (2) if the socket
  dropped mid-upload (`socket hang up`), the SDK's body-stream promise rejected
  unhandled. Fixed by uploading the file as a complete in-memory **buffer**
  (`addFileFromBuffer`) instead of a live stream: a buffer has a known length (no
  content-length error) and no socket-bound stream to re-reject. The cloud upload
  promise now never rejects — any failure is captured and exposed via
  `waitUntilStored()`, which `moveToStorage` checks inside a try/catch so a failed
  upload leaves the source file intact and is logged cleanly. (Files are moved one
  at a time, so peak memory is one file.) Thanks to Claude.
- **Fixed `TypeError: Cannot read properties of undefined (reading 'on')` when
  moving attachments between Meteor-Files/GridFS and Filesystem.** If a file's
  source binary was missing at its recorded location (e.g. a file left
  half-moved by an earlier interrupted run — storage says "gridfs" but the
  GridFS id reference is gone, or a filesystem path that no longer exists),
  `getReadStream()`/`getWriteStream()` returned `undefined` and `moveToStorage`
  (and `copyFile`) called `.on()` on it, throwing. Both now detect a missing
  read/write stream, log a clear message (which file, version, from→to storage,
  and why), **skip that file and leave the source intact** so no data is lost,
  and continue with the rest. Use the new "Repair file locations" button to fix
  the underlying inconsistent records. Thanks to Claude.
- **Fixed moving attachments/avatars from CollectionFS to Meteor-Files crashing
  the server, hanging the Admin Panel move at "File 1 / N", and leaving broken
  avatars.** Three problems in Admin Panel / Attachments / Move Attachment:
  - **Server crash on the first file** (`uncaughtException: TypeError: Cannot read
    properties of undefined (reading '_id')` at
    `AttachmentStoreStrategyGridFs.writeStreamFinished`). The current mongodb
    driver's GridFS `GridFSBucketWriteStream` `'finish'` event no longer passes
    the stored file document, so `finishedData._id` threw and killed the process
    (which is also why the move appeared stuck at "File 1 / 4" — the background
    job died mid-file and its persisted status was frozen). The GridFS strategy
    now reads the uploaded file id from the write stream itself (`gridFSFile._id`
    / `id`), the `'finish'` handler is wrapped so it can never crash the process,
    and a startup reconciliation clears a stale "running" move status left by a
    crashed run so the UI un-sticks and a new move can be started.
  - **Broken avatar after migrating avatars.** The bulk move only repointed card
    cover references (`cards.coverId`); a user's `profile.avatarUrl` still pointed
    at the deleted legacy `/cfs/files/avatars/<oldId>` URL, so the avatar rendered
    broken. The move now repoints `profile.avatarUrl` to the migrated avatar
    (`remapReferences` handles avatars), and migrated files stamp
    `meta.migratedFromId` so references can be repaired to the **exact** new file.
    The startup repair in `server/models/users.js` (previously a no-op: it
    string-replaced the URL prefix while keeping the now-deleted id and never
    saved the document) now points each affected user's avatar at their migrated
    Meteor-Files avatar — matched precisely by `meta.migratedFromId`, otherwise
    strictly by `userId` (newest migrated avatar first), so a user can only ever
    be given their own avatar, never another user's.
  - **`ObjectID` deprecation warning** from `models/lib/grid/createObjectId.js`
    (`MongoInternals.NpmModule.ObjectID` → `ObjectId`).

Thanks to Claude and xet7 !
2026-06-06 07:21:59 +03:00
Lauri Ojansivu f9ace4cde9 Updated translations. 2026-06-06 06:47:36 +03:00
Lauri Ojansivu 4677324425 Updated translations. 2026-06-06 05:23:18 +03:00
Lauri Ojansivu 7e5852c62c Updated ChangeLog. 2026-06-06 04:23:25 +03:00
Lauri Ojansivu e01f99eb52 Fix Dropdown list cannot be created with values.
**Fixed Dropdown custom field options not being addable / saving as empty.**
Creating a "Dropdown" custom field showed the "List Options" box, but pressing
Enter (or typing and saving) never added any option, and on the card the only
selectable value was `(none)`. The custom-fields sidebar component was migrated
from `BlazeComponent` to a plain `Template`, but the template still iterates the
options with `{{#each dropdownItems.get}}` — under BlazeComponent that resolved
to the instance's `dropdownItems` ReactiveVar, whereas a plain Template does not
expose instance variables to the template, so the list rendered nothing. Because
the options were never rendered as inputs, `getDropdownItems()` then overwrote
the ReactiveVar with the empty DOM on save and every entered value was dropped.
Fixed by re-adding the missing `dropdownItems` helper (returning the ReactiveVar),
matching the pattern the other migrated templates already use. Thanks to Claude.

**Fixed `Exception in global helper _` when opening the Create Custom Field
popup (and any translation containing a literal `%`).** i18n is configured with
a global sprintf post-processor (`postProcess: ["sprintf"]`), so every
translation is run through `i18next-sprintf-postprocessor`. The help text
`custom-field-stringtemplate-format` (`"Format (use %{value} as placeholder)"`)
contains a literal `%{value}` that sprintf cannot parse, so `vsprintf` threw and
crashed the global Blaze `_` translation helper — breaking that popup and any
string (in any language, including user translation overrides) that contains a
stray `%`. `TAPi18n.__` now retries without the sprintf post-processor when it
throws, returning the raw string (so `%{value}` is shown literally) instead of
crashing.

Thanks to rouceto1 and Claude.

Fixes #6357
2026-06-06 04:19:03 +03:00
Lauri Ojansivu 8ff9a41f2d Updated ChangeLog. 2026-06-06 04:14:25 +03:00
Lauri Ojansivu 075e86b00e **Fixed new checklists (and checklist items) on a newly added card not being
visible until logout/login.** The `board` publication batched checklists,
checklist items, comments and attachments into board-level cursors filtered by
`cardId: { $in: cardIds }`, where `cardIds` was a one-time snapshot. In
`reywood:publish-composite` a child cursor only re-runs when its parent (the
board) document changes, so the snapshot never refreshed when a card was added
— a checklist on a card created after subscribing matched no published card and
only appeared on the next subscribe (logout/login). This was a regression from
the "Optimized board loading" change, which had replaced the original reactive
per-card child cursors with these batched snapshots. Fixed by **denormalizing a
`boardId` field onto `Checklists` and `ChecklistItems`** so they can be
published with a single board-level cursor filtered by `boardId` — one cursor
per collection (keeping the load optimization) that still reacts to checklists
on newly added cards, because a new checklist is created already carrying the
board's id. `boardId` is set on insert (server `before.insert` hooks, plus
explicitly in the Trello/WeKan board importers, which use `direct.insert` and
bypass hooks), re-derived when a checklist/item or its card moves to another
card (`before.update` on `cardId`) or the card moves to another board
(`Cards.after.update` cascade), and backfilled for existing data by an
idempotent startup migration. New `{ boardId: 1 }` indexes were added on both
collections. Comments and attachments instead remain reactive as per-card
children of the cards cursor. Assigned-only board members still only receive
checklists for cards assigned to them (the board-level cursor falls back to the
assigned cards' ids for those roles).

Thanks to ahlgrimma and Claude !

Fixes #6361
2026-06-06 04:02:48 +03:00
Lauri Ojansivu f20080a603 Updated translations. 2026-06-06 03:26:10 +03:00
Lauri Ojansivu 3594d75b5d Updated ChangeLog. 2026-06-06 03:13:31 +03:00
Lauri Ojansivu 72767ad9a7 - Fixed SyncedCron crash.
**Fixed deleting archived lists (or many cards) crashing the server with
  `SyncedCron: Fatal error encountered (unhandledRejection): TypeError: Cannot
  read properties of undefined (reading 'boardId')` at
  `server/models/checklistItems.js`.** Deleting a list cascades into removing its
  cards, and each card's checklists, checklist items, comments and attachments
  (`cardRemover` in `models/cards.js`). `Cards.before.remove` called `cardRemover`
  **without `await`** (and was not `async`), so the card document was deleted
  first and the cascade then ran with the parent card already gone; the
  `ChecklistItems.before.remove` / `Checklists.before.remove` hooks dereferenced
  the now-undefined card (`card.boardId`) and threw, and because the promise was
  unhandled, SyncedCron caught the rejection and tore down all running cron jobs.
  Fixed by making `Cards.before.remove` `async` and awaiting `cardRemover` (so
  sub-items are removed while the card still exists), and the REST card-delete
  handler now runs `cardRemover` before removing the card. As defense in depth,
  every card-activity hook and helper that looked up a card and used its
  `boardId` / `listId` / `swimlaneId` now skips (with a warning) when the parent
  card — or, for checklist-completion activities, the parent checklist — is
  missing, instead of throwing: `before.remove` on checklist items and
  checklists, `Checklists.after.insert`, the `Cards.before.update` timing
  activity, and the shared `itemCreation` / `publishCheckActivity` /
  `publishChekListCompleted` / `publishChekListUncompleted` / `commentCreation`
  helpers (matching the guards already present in
  `server/models/cardComments.js`).

Thanks to titver968 and Claude !

Fixes #6366
2026-06-06 03:10:13 +03:00
Lauri Ojansivu 7db386fda3 Updated ChangeLog. 2026-06-06 02:55:55 +03:00
Lauri Ojansivu 39f3c89b0a Fixed upgrade crash.
Thanks to poc-sm and Claude !

Fixes #6367
2026-06-06 02:53:58 +03:00
Lauri Ojansivu 15a598525f Updated ChangeLog. 2026-06-06 02:10:21 +03:00
Lauri Ojansivu d30803276d - **Fixed Admin Panel / Attachments / Move Attachment doing nothing / crashing.**
Several issues in the bulk move:
- The GridFS source matcher required `meta.gridFsFileId` to be *absent*, but
  Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero
  files and "nothing happened". The matcher now recognizes real GridFS files
  (`versions.*.storage === 'gridfs'` or a `meta.gridFsFileId` reference), and a
  "nothing to move" message is shown when a source is empty instead of silently
  doing nothing.
- Moving files crashed the whole server with
  `FilesCollection#findOne() not available in server` — `ReactiveCache.getAttachment`
  used a synchronous `findOne()`; it now uses `findOneAsync()`. The background
  job is also hardened so a single failing file is skipped instead of crashing
  the server via an unhandled rejection.
- The attachment **copy** API now honours the admin API upload limits (it
  previously skipped them).
- **Fixed the "MongoDB Meteor-Files" file-count statistic** in Admin Panel /
  Attachments, which counted *every* attachment metadata document (so files on
  the Filesystem were wrongly reported as being in GridFS). It now counts only
  attachments actually stored in GridFS, consistent with the move tool. Also
  renamed the mislabeled "Mongo-Files" column to "Meteor-Files". Thanks to Claude.
- **Unified attachment/avatar storage migration: move any → any.** Admin Panel /
  Attachments / Move Attachment can now move **Attachments, Avatars, or both**,
  from **any source to any destination** across Filesystem, Meteor-Files GridFS,
  Cloud (S3/Azure/GCS) and legacy CollectionFS GridFS. The default source is
  "All Read-enabled storages" (every backend whose Read flag is enabled and whose
  settings work), so everything can be consolidated into one destination in a
  single run. All metadata is preserved (board / swimlane / list / card / user /
  uploaded date / name / type / size), attachment cover references
  (`cards.coverId`) are remapped when an id changes, and the legacy source is
  deleted only after the new copy is verified. Thanks to Claude.
- **Legacy CollectionFS GridFS is now a first-class storage backend** (read,
  migrate-from, and export-to) for both attachments and avatars, via the new
  `models/lib/collectionFsStore.js` (binary keyed by `copies.<coll>.key` in the
  `cfs_gridfs.<coll>` bucket, metadata in `cfs.<coll>.filerecord`). Thanks to Claude.
- **The storage strategy layer is now collection-aware.** `moveToStorage` and the
  GridFS/Cloud strategies previously hard-coded the `Attachments` collection, so
  moving avatars to GridFS/cloud would have updated the wrong collection. The
  factory now carries its collection (`Attachments` or `Avatars`), so **avatars
  can be stored in Meteor-Files GridFS and cloud**, not only on the filesystem.
  Attachment behavior is unchanged. Thanks to Claude.
- **Read legacy CollectionFS attachments and avatars in place** (without
  migrating). The backward-compatibility layer
  (`models/lib/attachmentBackwardCompatibility.js`) was broken — it looked up the
  GridFS binary by the filerecord `_id` and by filename instead of by
  `ObjectId(copies.<coll>.key)`, so legacy files were never found. It is fixed and
  generalized for attachments and avatars. Legacy attachments now appear in the
  card attachment gallery (new `legacyBoardAttachments` publication) and stream
  from the `cfs_gridfs.attachments` bucket, and legacy avatars
  (`/cfs/files/avatars/<id>` URLs) are served from the `cfs_gridfs.avatars`
  bucket instead of redirecting to a 404.

Thanks to Claude.
2026-06-06 02:06:16 +03:00
Lauri Ojansivu be63a357dd Updated ChangeLog. 2026-06-05 23:47:19 +03:00
Lauri Ojansivu c71a97cba2 **Fixed OpenAPI REST API documentation generation**, which had been broken
since after WeKan v7.93 and only generated docs for the `login`/`register`
endpoints (2 operations) instead of the full API. The Meteor 3 migration moved
the REST routes from `models/*.js` (`JsonRoutes.add(...)`) into
`server/models/*.js` (`WebApp.handlers.get/post/put/delete(...)`) and
introduced optional chaining (`?.`) that the `esprima` Python parser cannot
read, so `openapi/generate_openapi.py` silently skipped every route file.
The generator now understands both routing styles, scans both `models/` and
`server/models/`, downlevels modern JS syntax so files parse, handles the
`type: Array` SimpleSchema idiom, and `releases/rebuild-docs.sh` works directly
with Python 3.12.x (PEP 668). The generated `public/api/wekan.yml` /
`wekan.html` now cover the full API again (89 operations / 61 paths).
Thanks to Claude.

**Documented the attachment / file REST API in the OpenAPI docs** — file
upload, download, info, listing a board's files, copy, move and delete (the
`/api/attachment/*` endpoints registered via `WebApp.handlers.use()`, which the
generator cannot auto-discover) are now documented in `openapi/extra_paths.yml`
and injected into the generated docs, including their authentication and
permission requirements. Thanks to Claude.

**Security: attachment write operations in the REST/DDP API now require board
write access, not just membership (CWE-862, CWE-639).** Both attachment API
implementations (`server/routes/attachmentApi.js` HTTP routes and
`server/attachmentApi.js` Meteor methods) gated upload / copy / move / delete
on `board.hasMember()`, which is true for any active member regardless of role.
This let read-only, comment-only, no-comments, worker and assigned-only board
members add, copy, move and delete attachments through the API — actions the UI
forbids for those roles. They now require board write access (global site
admins still allowed); read operations (download / list / info) keep
membership-level access. Added security tests in
`server/lib/tests/attachmentApi.tests.js`. Thanks to Claude.

**The attachment copy API now honours the admin "Admin Panel / Attachments"
API transfer limits.** Copying an attachment creates a new attachment but
skipped the `apiUploadBlocked` / `apiUploadMaxBytes` checks that upload
enforces; copy now respects them in both API implementations. Thanks to Claude.

**SVG image uploads are now sanitized instead of rejected.** Uploaded SVGs
(attachments and avatars) are cleaned in place in `onAfterUpload` via the new
`models/lib/sanitizeSvg.js`, which removes JavaScript (`<script>`, inline
`on*=` event handlers, `javascript:`/`vbscript:` URIs, `<foreignObject>` /
`<iframe>` / `<object>` / `<embed>` and similar active content) and XML loops
(`<!DOCTYPE>` / `<!ENTITY>` entity-expansion / XXE constructs and
`<?xml-stylesheet?>`), so SVG images can be uploaded safely.

Thanks to Claude.
2026-06-05 23:44:04 +03:00
Lauri Ojansivu 357de728c0 Fix [CloneBleed](https://wekan.fi/hall-of-fame/clonebleed/) and other security issues. Added more security tests to test suite.
Thanks to dizconnectz for the coordinated disclosure, and Claude.
2026-06-05 21:40:24 +03:00
Lauri Ojansivu db3c0c18f9 Updated ChangeLog. 2026-06-05 19:09:11 +03:00
Lauri Ojansivu 45706292e7 Updated translations. 2026-06-05 19:08:57 +03:00
Lauri Ojansivu 29b3f52e50 Updated to Meteor 3.5-rc.1 .
Thanks to Meteor developers !
2026-06-05 19:04:12 +03:00
Lauri Ojansivu f81a0d1537 Merge pull request #6365 from italojs/main
Update DDP transport and reactivity order for multi-tenant deployment
2026-06-05 18:57:44 +03:00
Lauri Ojansivu e613e8763b Updated Docker Meteor 3 settings. Part 2.
Thanks to italojs and xet7 !

Related https://github.com/meteor/meteor/pull/14112#issuecomment-4595290396
2026-06-02 00:03:27 +03:00
Lauri Ojansivu 3d17810293 Updated Docker Meteor 3 settings.
Thanks to italojs and xet7 !

Related https://github.com/meteor/meteor/pull/14112#issuecomment-4595290396
2026-06-02 00:00:22 +03:00
Lauri Ojansivu 1871873da0 Caddyfile: Separate websocket port not required.
Thanks to italojs !

Related https://github.com/meteor/meteor/pull/14112#issuecomment-4596252210
2026-06-01 23:40:52 +03:00
Lauri Ojansivu 15b3213a4d Fix typos 2026-06-01 22:51:20 +03:00
Lauri Ojansivu 4265288417 Updates 2026-06-01 22:49:15 +03:00
Lauri Ojansivu a36d6f9326 Merge branch 'main' of github.com:wekan/wekan 2026-06-01 22:45:07 +03:00
Lauri Ojansivu 3c50f90b51 Add separate websocket port 2026-06-01 22:44:27 +03:00
italo jose 24a4b8e5ee Refactor MongoDB service entrypoint and command for improved readability 2026-06-01 16:22:46 -03:00
italo jose e676f96905 Update DDP transport and reactivity order for multi-tenant deployment; add docker-compose example 2026-06-01 15:54:54 -03:00
Lauri Ojansivu a903154ab2 Updated ChangeLog. 2026-05-31 14:38:24 +03:00
Lauri Ojansivu beedf2fefa Update Windows MongoDB.
Thanks to xet7 !
2026-05-31 14:36:32 +03:00
Lauri Ojansivu dee5711000 v9.34 2026-05-31 13:42:43 +03:00
Lauri Ojansivu e93c049e8c Updated translations. 2026-05-31 13:38:57 +03:00
Lauri Ojansivu fc10ce4259 Fix to not list all boards etc at Admin Panel / Attachments / Move Attachment.
Thanks to xet7 !
2026-05-31 13:35:34 +03:00
Lauri Ojansivu 2573034149 v9.33 2026-05-31 12:46:36 +03:00
Lauri Ojansivu a1fc2a91dc 9.33 2026-05-31 12:40:33 +03:00
Lauri Ojansivu e86c37e636 Updated translations. 2026-05-31 12:18:56 +03:00
Lauri Ojansivu 8b71386b90 Updated translations. 2026-05-31 11:51:31 +03:00
Lauri Ojansivu 2edef5b4ab Updated translations. 2026-05-31 11:50:10 +03:00
Lauri Ojansivu d2a25ae9a6 Updated ChangeLog. 2026-05-31 11:15:12 +03:00
Lauri Ojansivu 6f021aa387 Mobile / touch fixes (Fairphone 4 postmarketOS Firefox and Fairphone 4 Ubuntu Touch Morph browser; iPhone 12 Mini was already correct):
- Fixed one-finger drag-to-scroll not working in the All Boards view and the
  Calendar view on Fairphone 4 Firefox and Ubuntu Touch Morph. These browsers
  do not emit the synthetic mouse events the `@wekanteam/dragscroll` library
  relies on, so scrolling only worked in the Swimlanes/Lists views. Added a new
  native touch-scrolling helper `client/lib/dragscrollTouch.js` that scrolls the
  nearest `.dragscroll` container with a one-finger drag (iOS is skipped, since
  native momentum scrolling already works there), and added the `dragscroll`
  class to the All Boards list and the Calendar view.
- Fixed all text, buttons, the All Boards tables and the top bars (header and
  the second bar) rendering about 2x too large on Fairphone 4. `getMobileMode()`
  defaulted to mobile mode only for iPhone, so every other phone fell back to
  desktop mode, whose large desktop sizing looks oversized on a small phone
  screen. The default is now mobile mode for any phone-sized touch device
  (iPhone, Android/Mobile browsers, Ubuntu Touch, or a coarse-pointer touch
  screen with viewport width ≤ 800px); desktop browsers stay in desktop mode and
  the user's own Mobile/Desktop toggle still takes priority.
- Fixed not being able to reorder swimlanes and lists by dragging their drag
  handle on touch devices. The new touch-scroll helper was hijacking touches
  that started on a drag handle; `.handle` / `.ui-sortable-handle` elements are
  now excluded from touch-scrolling so dragging the handle reorders as expected.
- Fixed a card duplicating into two cards when it was dragged by its title-row
  drag handle while its Card Details panel was open. The card sortable in
  `client/components/lists/list.js` was re-initialized on every list re-render
  without first destroying the previous instance, so two `stop` handlers fired
  per drag. It now destroys any existing sortable before re-initializing, the
  same guard already used for swimlanes.

Attachment storage:

- Added configurable attachment storage backends in the admin panel: choose the
  default save storage and store attachments on the local filesystem, GridFS, or
  cloud object storage — S3-compatible (AWS S3, MinIO, Cloudflare R2, Backblaze
  B2, Wasabi, DigitalOcean Spaces), Azure Blob Storage, or Google Cloud Storage
  — via the `@tweedegolf/storage-abstraction` adapters (new
  `models/lib/cloudStorage.js`). Includes a "Test connection" action and
  English translations for the new settings.
- Added a server-side bulk attachment move (new `server/attachmentBulkMove.js`
  and `models/attachmentBulkMoveStatus.js`) that moves all attachments from one
  storage backend to another as a background job whose progress survives the
  admin navigating away from or closing the page.

Thanks to xet7.
2026-05-31 11:12:41 +03:00
351 changed files with 43336 additions and 3421 deletions

No files matched your search

+2 -1
View File
@@ -1,3 +1,4 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+88 -13
View File
@@ -26,13 +26,33 @@ jobs:
node-version: '24'
cache: 'npm'
- name: Cache Meteor installation
uses: actions/cache@v5
with:
path: ~/.meteor
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
restore-keys: meteor-
- name: Cache Meteor build (.meteor/local)
uses: actions/cache@v5
with:
path: .meteor/local
key: meteor-local-unit-${{ hashFiles('.meteor/versions') }}
restore-keys: meteor-local-unit-
- name: Install Meteor
run: curl https://install.meteor.com/ | sh
run: |
if [ ! -x ~/.meteor/meteor ]; then
curl https://install.meteor.com/ | sh
fi
echo "$HOME/.meteor" >> "$GITHUB_PATH"
- name: Install npm dependencies
run: npm ci
- name: Run Meteor unit tests
# First Meteor build is slow; allow time before the test driver starts.
timeout-minutes: 45
run: meteor test --once --driver-package meteortesting:mocha
env:
TEST_WATCH: '0'
@@ -66,8 +86,26 @@ jobs:
node-version: '24'
cache: 'npm'
- name: Cache Meteor installation
uses: actions/cache@v5
with:
path: ~/.meteor
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
restore-keys: meteor-
- name: Cache Meteor build (.meteor/local)
uses: actions/cache@v5
with:
path: .meteor/local
key: meteor-local-e2e-${{ hashFiles('.meteor/versions') }}
restore-keys: meteor-local-e2e-
- name: Install Meteor
run: curl https://install.meteor.com/ | sh
run: |
if [ ! -x ~/.meteor/meteor ]; then
curl https://install.meteor.com/ | sh
fi
echo "$HOME/.meteor" >> "$GITHUB_PATH"
- name: Install app npm dependencies
run: npm ci
@@ -85,13 +123,22 @@ jobs:
MONGO_URL=mongodb://localhost:27017/wekan_test \
ROOT_URL=http://localhost:3000 \
PORT=3000 \
meteor run --settings settings.json &
# Wait up to 120 s for WeKan to become available
for i in $(seq 1 24); do
curl -sf http://localhost:3000 > /dev/null && break
echo "Waiting for WeKan... attempt $i"
sleep 5
meteor run --settings settings.json > wekan-run.log 2>&1 &
# The first Meteor build is slow, so wait up to 20 minutes for the
# server to start responding before giving up.
for i in $(seq 1 120); do
if curl -sf http://localhost:3000 > /dev/null; then
echo "WeKan is up after $((i * 10))s"
break
fi
echo "Waiting for WeKan... attempt $i/120"
sleep 10
done
if ! curl -sf http://localhost:3000 > /dev/null; then
echo "WeKan failed to start. Last 200 log lines:"
tail -n 200 wekan-run.log
exit 1
fi
env:
DISABLE_CAPTCHA: 'true'
@@ -145,8 +192,26 @@ jobs:
node-version: '24'
cache: 'npm'
- name: Cache Meteor installation
uses: actions/cache@v5
with:
path: ~/.meteor
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
restore-keys: meteor-
- name: Cache Meteor build (.meteor/local)
uses: actions/cache@v5
with:
path: .meteor/local
key: meteor-local-regression-${{ hashFiles('.meteor/versions') }}
restore-keys: meteor-local-regression-
- name: Install Meteor
run: curl https://install.meteor.com/ | sh
run: |
if [ ! -x ~/.meteor/meteor ]; then
curl https://install.meteor.com/ | sh
fi
echo "$HOME/.meteor" >> "$GITHUB_PATH"
- name: Install npm dependencies
run: npm ci
@@ -159,11 +224,21 @@ jobs:
MONGO_URL=mongodb://localhost:27017/wekan_regression \
ROOT_URL=http://localhost:3000 \
PORT=3000 \
meteor run &
for i in $(seq 1 24); do
curl -sf http://localhost:3000 > /dev/null && break
sleep 5
meteor run > wekan-run.log 2>&1 &
# The first Meteor build is slow, so wait up to 20 minutes.
for i in $(seq 1 120); do
if curl -sf http://localhost:3000 > /dev/null; then
echo "WeKan is up after $((i * 10))s"
break
fi
echo "Waiting for WeKan... attempt $i/120"
sleep 10
done
if ! curl -sf http://localhost:3000 > /dev/null; then
echo "WeKan failed to start. Last 200 log lines:"
tail -n 200 wekan-run.log
exit 1
fi
env:
DISABLE_CAPTCHA: 'true'
+398 -252
View File
@@ -1,40 +1,115 @@
name: Release All Platforms
# Triggered manually via GitHub CLI or Actions UI.
# Triggered manually via GitHub CLI or the Actions UI.
# Usage:
# gh workflow run release-all.yml -f old_version=8.42 -f new_version=8.43
# gh workflow run release-all.yml -f old_version=9.35 -f new_version=9.36
#
# Or use the convenience script (commits pending changes, pushes, and triggers):
# ./releases/release-all.sh 8.42 8.43
# Or use the convenience script (bumps versions, commits, pushes, then triggers):
# ./releases/release-all.sh 9.35 9.36
#
# Builds only the platforms MongoDB ships server binaries for (amd64, arm64,
# win64, mac); MongoDB 7.x is the default database. s390x/ppc64le are not built
# because MongoDB has no server binaries for them.
#
# This workflow does EVERYTHING for a release remotely (nothing is built or
# pushed on the maintainer's machine), running jobs in parallel where possible:
# - bump: runs releases/version.sh + releases/rebuild-docs.sh on the runner
# (bumps Node/MongoDB/WeKan versions, rebuilds API docs), then
# commits and pushes the bump to wekan/wekan main
# - prepare: tags the bumped main as v<new_version> and extracts release notes
# - website: updates the wekan/wekan.fi repo (install page + API docs) and pushes
# - charts: updates and publishes the wekan/charts Helm chart and pushes
# - build-*: builds the amd64 bundle (full Meteor 3 build), then rebuilds native
# modules for arm64, win64, mac-arm64 from that amd64 base
# - release: creates the GitHub Release with the newest CHANGELOG.md notes and
# uploads every wekan-<version>-<platform>.zip bundle
# - docker: builds and pushes the multi-arch image to Docker Hub, Quay.io, GHCR
# - snap: builds the snap on Launchpad (amd64 + arm64), pushes it to the Snap
# Store and attaches the .snap files to the GitHub Release
#
# Required GitHub Secrets:
# GHCR_AUTH - base64(username:token) for ghcr.io
# DOCKERHUB_AUTH - base64(username:token) for Docker Hub
# QUAY_AUTH - base64(username:token) for Quay.io
# SNAP_AUTH - Snapcraft store credentials
# GHCR_AUTH - base64("username:token") for ghcr.io
# DOCKERHUB_AUTH - base64("username:token") for Docker Hub
# QUAY_AUTH - base64("username:token") for Quay.io
# SNAP_AUTH - Snap Store credentials (output of `snapcraft export-login`)
# LP_CREDENTIALS - base64 of the Launchpad credentials file used by
# `snapcraft remote-build` (see releases/snap-build.sh notes)
# WEKAN_REPO_TOKEN - Personal Access Token with write access to wekan/wekan.fi
# and wekan/charts, used by the website + charts jobs to push
# to those repos (the built-in GITHUB_TOKEN only covers
# wekan/wekan). See releases/create-github-secrets.sh.
on:
workflow_dispatch:
inputs:
old_version:
description: 'Previous version number (e.g. 8.42, without v prefix)'
description: 'Previous version number (e.g. 9.35, without v prefix)'
required: true
type: string
new_version:
description: 'New version number (e.g. 8.43, without v prefix)'
description: 'New version number (e.g. 9.36, without v prefix)'
required: true
type: string
permissions:
contents: write
packages: write
pages: write
id-token: write
# Node.js major version that Meteor 3.5-beta bundles. Used for native module
# rebuilds on every platform — it must match the amd64 build's Node ABI.
env:
NODE_VERSION: '24'
jobs:
# ─── 1. Extract version and release notes ───────────────────────────────────
# ─── 0. Bump versions + rebuild API docs, commit to main ────────────────────
# Runs releases/version.sh (probes upstream for the newest Node.js/MongoDB,
# bumps every version reference) and releases/rebuild-docs.sh (regenerates
# public/api/wekan.yml + wekan.html), then commits and pushes to main so the
# tag, builds, website and charts jobs all see the bumped sources.
bump:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install version-bump and docs build dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 python3-pip curl wget
- name: Bump versions and rebuild API docs
env:
# Builds fetch dependencies from upstream themselves, so skip the slow
# pre-download of Node/MongoDB tarballs that the local flow does.
RELEASE_SKIP_DEP_DOWNLOAD: '1'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
./releases/version.sh "${{ inputs.old_version }}" "${{ inputs.new_version }}"
./releases/rebuild-docs.sh "${{ inputs.new_version }}"
- name: Commit and push version bump
run: |
git add --all
if git diff --cached --quiet; then
echo "No version changes to commit."
else
git commit -m "Bump versions for v${{ inputs.new_version }}"
git push origin main
fi
# ─── 1. Tag the release and extract release notes ───────────────────────────
prepare:
needs: bump
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
@@ -42,29 +117,42 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
ref: main
fetch-depth: 0
- name: Set version from input
- name: Normalise version from input
id: version
run: |
VERSION="${{ needs.prepare.outputs.version }}"
VERSION="${{ inputs.new_version }}"
VERSION="${VERSION#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Create and push tag v${{ steps.version.outputs.version }}
run: |
VERSION="${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists, leaving it as is."
else
git tag -a "v$VERSION" -m "v$VERSION"
git push origin "v$VERSION"
fi
- name: Extract release notes from CHANGELOG.md
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
# Try to find the version-specific section first, then fall back to Upcoming
# Prefer a version-specific "# v<version> ..." section, otherwise fall
# back to the topmost "# Upcoming/Upcomig WeKan ® release" section.
NOTES=$(python3 - <<PYEOF
import re, sys
version = "$VERSION"
with open("CHANGELOG.md", encoding="utf-8") as f:
content = f.read()
# Pattern: section heading through (but not including) the next heading
patterns = [
rf"(# v{re.escape(version)} .*?)(?=\n# v[0-9]|\Z)",
r"(# Upcoming WeKan.*?)(?=\n# v[0-9]|\Z)",
r"(# Upcom\w* WeKan.*?)(?=\n# v[0-9]|\Z)",
]
for pat in patterns:
m = re.search(pat, content, re.DOTALL)
@@ -74,28 +162,98 @@ jobs:
print(f"WeKan v{version}")
PYEOF
)
echo "content<<GHEOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "GHEOF" >> $GITHUB_OUTPUT
{
echo "content<<GHEOF"
echo "$NOTES"
echo "GHEOF"
} >> "$GITHUB_OUTPUT"
# ─── 1b. Update the wekan.fi website repo (parallel) ────────────────────────
# Bumps the version/Meteor/Node/MongoDB strings on the install page and copies
# the freshly rebuilt API docs into wekan.fi/api/v<new>, then commits + pushes
# to wekan/wekan.fi. Runs as soon as `bump` has pushed the bumped sources.
website:
needs: bump
runs-on: ubuntu-latest
steps:
- name: Checkout wekan repo (bumped main, with rebuilt API docs)
uses: actions/checkout@v6
with:
ref: main
path: wekan
- name: Checkout wekan.fi website repo
uses: actions/checkout@v6
with:
repository: wekan/wekan.fi
token: ${{ secrets.WEKAN_REPO_TOKEN }}
path: wekan.fi
- name: Update and push website
env:
WEBDIR: ${{ github.workspace }}/wekan.fi
WEKANREPODIR: ${{ github.workspace }}/wekan
RELEASE_PUSH_WEBSITE: '1'
run: |
cd wekan.fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
cd ..
./wekan/releases/release-website.sh "${{ inputs.old_version }}" "${{ inputs.new_version }}"
# ─── 1c. Update and publish the Helm chart repo (parallel) ──────────────────
# Bumps the WeKan version in wekan/charts, packages the chart and publishes it
# to the gh-pages Helm index, then pushes both branches. Runs in parallel with
# the platform builds; packaging the chart does not need the built artifacts.
charts:
needs: bump
runs-on: ubuntu-latest
steps:
- name: Checkout wekan repo (for releases/release-charts.sh + version refs)
uses: actions/checkout@v6
with:
ref: main
path: wekan
- name: Checkout wekan/charts repo (main + gh-pages)
uses: actions/checkout@v6
with:
repository: wekan/charts
token: ${{ secrets.WEKAN_REPO_TOKEN }}
fetch-depth: 0
path: charts
- name: Install Helm
uses: azure/setup-helm@v4
- name: Fetch gh-pages branch (release-charts.sh switches to it)
run: |
cd charts
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin '+refs/heads/*:refs/remotes/origin/*'
git branch --track gh-pages origin/gh-pages 2>/dev/null || true
- name: Release and publish chart
env:
CHARTS_DIR: ${{ github.workspace }}/charts
run: |
./wekan/releases/release-charts.sh "${{ inputs.new_version }}"
# ─── 2. Build amd64 bundle (full Meteor 3 build) ────────────────────────────
# Meteor 3.5-beta.7 installer supports only: Linux x86_64, Linux aarch64, macOS, Windows.
# It explicitly rejects armhf, s390x, and ppc64le at install time.
# Therefore the full Meteor build always runs here on x86_64.
# All other Linux architectures then rebuild only the native npm modules
# from this bundle (much faster than a full Meteor build).
# Node.js version must match what Meteor 3.5-beta.7 bundles (detected automatically).
# Meteor 3.5-beta installer supports only Linux x86_64/aarch64, macOS, Windows.
# The full Meteor build always runs here on x86_64; every other platform then
# only rebuilds the native npm modules from this bundle.
build-amd64:
needs: prepare
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
ref: refs/tags/v${{ inputs.new_version }}
ref: refs/tags/v${{ needs.prepare.outputs.version }}
- name: Cache Meteor installation
uses: actions/cache@v5
@@ -115,11 +273,9 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip
# Pin to detected latest Node.js 24.x — must match Meteor's bundled version.
# Using the same version for build and npm rebuild ensures consistent native module ABI.
- uses: actions/setup-node@v6
with:
node-version: '${{ needs.prepare.outputs.version }}'
node-version: ${{ env.NODE_VERSION }}
- name: Install Meteor
run: |
@@ -134,6 +290,11 @@ jobs:
(meteor update --npm 2>/dev/null || true) && meteor npm install
meteor build .build --directory
- name: Install server npm modules
run: |
cd .build/bundle/programs/server
npm install
- name: Create amd64 zip
run: |
cd .build
@@ -146,30 +307,18 @@ jobs:
retention-days: 1
# ─── 3. Build arm64 bundle (rebuild native modules natively) ─────────────────
# Meteor 3.5-beta.7 does support Linux aarch64, but running a full Meteor build a
# second time would double the build time for the same result. npm rebuild on
# the native arm64 GitHub Actions runner is much faster and produces the same
# platform-specific native modules.
build-arm64:
needs: [prepare, build-amd64]
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION_ARM64: ''
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
- name: Set Node.js version for arm64
id: node_arm64
run: |
echo "NODE_VERSION_ARM64=24" >> $GITHUB_ENV
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
@@ -192,14 +341,13 @@ jobs:
popd > /dev/null
rm -rf "$TMP"
fi
# Use the latest available Node.js 24.x slim image for arm64
docker run --rm --platform linux/arm64 \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24-slim \
node:${NODE_VERSION}-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create arm64 zip
- name: Create arm64 zip
run: |
zip -r wekan-${VERSION}-arm64.zip bundle
@@ -209,195 +357,6 @@ jobs:
path: wekan-${{ needs.prepare.outputs.version }}-arm64.zip
retention-days: 1
# ─── 4. Build s390x bundle (rebuild native modules via QEMU container) ───────
# Meteor 3.5-beta.7 does NOT support s390x (installer exits with error on that arch).
# GitHub Actions has no native s390x runners.
# Solution: QEMU emulation + node:<version>-slim (s390x variant) for npm rebuild.
build-s390x:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 90
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION_S390X: ''
steps:
- name: Set up QEMU for s390x
uses: docker/setup-qemu-action@v4
with:
platforms: s390x
- name: Install unzip/zip
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Set Node.js version for s390x
id: node_s390x
run: |
echo "NODE_VERSION_S390X=24" >> $GITHUB_ENV
- name: Rebuild native modules for s390x in container
run: |
unzip wekan-${VERSION}-amd64.zip
rm -rf bundle/programs/server/node_modules
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
if [ -d "$BCRYPT_DIR" ]; then
rm -rf "$BCRYPT_DIR"
TMP=$(mktemp -d)
pushd "$TMP" > /dev/null
npm pack bcrypt@6.0.0
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
cd bcrypt && npm install --ignore-scripts && cd ..
cp -a bcrypt "$BCRYPT_DIR"
popd > /dev/null
rm -rf "$TMP"
fi
# Use the latest available Node.js 24.x slim image for s390x
docker run --rm --platform linux/s390x \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create s390x zip
run: |
zip -r wekan-${VERSION}-s390x.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-s390x
path: wekan-${{ needs.prepare.outputs.version }}-s390x.zip
retention-days: 1
# ─── 4b. Build ppc64le bundle (rebuild native modules via QEMU container) ────
# Meteor 3.5-beta.7 does NOT support ppc64le (installer exits with error on that arch).
# GitHub Actions has no native ppc64le runners.
# Solution: QEMU emulation + node:<version>-slim (ppc64le variant) for npm rebuild.
build-ppc64le:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 90
env:
VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION: ${{ needs.prepare.outputs.version }}
NODE_VERSION_PPC64LE: ''
steps:
- name: Set up QEMU for ppc64le
uses: docker/setup-qemu-action@v4
with:
platforms: ppc64le
- name: Install unzip/zip
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Set Node.js version for ppc64le
id: node_ppc64le
run: |
echo "NODE_VERSION_PPC64LE=24" >> $GITHUB_ENV
- name: Rebuild native modules for ppc64le in container
run: |
unzip wekan-${VERSION}-amd64.zip
rm -rf bundle/programs/server/node_modules
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
if [ -d "$BCRYPT_DIR" ]; then
rm -rf "$BCRYPT_DIR"
TMP=$(mktemp -d)
pushd "$TMP" > /dev/null
npm pack bcrypt@6.0.0
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
cd bcrypt && npm install --ignore-scripts && cd ..
cp -a bcrypt "$BCRYPT_DIR"
popd > /dev/null
rm -rf "$TMP"
fi
# Use the latest available Node.js 24.x slim image for ppc64le
docker run --rm --platform linux/ppc64le \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create ppc64le zip
run: |
zip -r wekan-${VERSION}-ppc64le.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-ppc64le
path: wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
retention-days: 1
# ─── 4c. Build armhf bundle — COMMENTED OUT ─────────────────────────────────
# No official Node.js binaries exist for armhf (arm/v7).
#build-armhf:
# needs: [prepare, build-amd64]
# runs-on: ubuntu-latest
# timeout-minutes: 120
# env:
# VERSION: ${{ needs.prepare.outputs.version }}
# steps:
# - name: Set up QEMU for arm/v7
# uses: docker/setup-qemu-action@v4
# with:
# platforms: arm
#
# - name: Install unzip/zip
# run: sudo apt-get update && sudo apt-get install -y unzip zip
#
# - name: Download amd64 bundle
# uses: actions/download-artifact@v8
# with:
# name: bundle-amd64
#
# - name: Rebuild native modules for armhf in container
# run: |
# unzip wekan-${VERSION}-amd64.zip
# rm -rf bundle/programs/server/node_modules
# # Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
# # which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
# BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
# if [ -d "$BCRYPT_DIR" ]; then
# rm -rf "$BCRYPT_DIR"
# TMP=$(mktemp -d)
# pushd "$TMP" > /dev/null
# npm pack bcrypt@6.0.0
# mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
# cd bcrypt && npm install --ignore-scripts && cd ..
# cp -a bcrypt "$BCRYPT_DIR"
# popd > /dev/null
# rm -rf "$TMP"
# fi
# # node:<version>-slim arm/v7 — same Node.js version as Meteor bundled
# docker run --rm --platform linux/arm/v7 \
# -v "$(pwd)/bundle:/bundle" \
# -w /bundle/programs/server \
# node:${NODE_VERSION}-slim \
# sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
#
# - name: Clean up and create armhf zip
# run: |
# zip -r wekan-${VERSION}-armhf.zip bundle
#
# - uses: actions/upload-artifact@v7
# with:
# name: bundle-armhf
# path: wekan-${{ needs.prepare.outputs.version }}-armhf.zip
# retention-days: 1
# ─── 5. Build win64 bundle (rebuild native modules on Windows) ───────────────
build-win64:
needs: [prepare, build-amd64]
@@ -406,14 +365,17 @@ jobs:
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
# Checked out into src/ so start-wekan.bat and snapcraft.yaml (which pins
# the MongoDB tools/mongosh versions) are available; the downloaded bundle
# is extracted alongside it in the workspace root.
- uses: actions/checkout@v6
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
path: src
- uses: actions/setup-node@v6
with:
node-version: '${{ needs.prepare.outputs.version }}'
## With Node 24 these are already preinstalled at GitHub Windows runners
#- name: Install node-gyp and build tools
# run: |
# npm install -g windows-build-tools --vs2019 2>$null; exit 0
node-version: ${{ env.NODE_VERSION }}
- name: Install 7-zip
run: choco install -y 7zip
@@ -443,12 +405,45 @@ jobs:
fi
- name: Rebuild native modules for Windows
shell: cmd
shell: bash
run: |
cd bundle\programs\server
rmdir /s /q node_modules
cd bundle/programs/server
rm -rf node_modules
npm install
cd ..\..\..
- name: Add MongoDB tools, mongosh and start-wekan.bat to the bundle
shell: bash
run: |
set -euo pipefail
# Use the same MongoDB tools/mongosh versions as the snap, which
# releases/version.sh keeps at the newest release for each bundle.
TOOLS_VER=$(grep -oE 'mongodb-database-tools-ubuntu2204-\$\{TOOLS_ARCH\}-[0-9]+\.[0-9]+\.[0-9]+' src/snapcraft.yaml \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
MONGOSH_VER=$(grep -oE 'mongosh-[0-9]+\.[0-9]+\.[0-9]+-linux' src/snapcraft.yaml \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo "MongoDB Database Tools: $TOOLS_VER mongosh: $MONGOSH_VER"
# MongoDB Database Tools (Windows): bsondump, mongodump, mongoexport,
# mongofiles, mongoimport, mongorestore, mongostat, mongotop.
TOOLS_DIR="mongodb-database-tools-windows-x86_64-${TOOLS_VER}"
curl -fL -o tools.zip \
"https://fastdl.mongodb.org/tools/db/${TOOLS_DIR}.zip"
7z x -y tools.zip > /dev/null
cp "${TOOLS_DIR}/bin/"*.exe bundle/
# mongosh (Windows): mongosh.exe + mongosh_crypt_v1.dll.
MONGOSH_DIR="mongosh-${MONGOSH_VER}-win32-x64"
curl -fL -o mongosh.zip \
"https://downloads.mongodb.com/compass/${MONGOSH_DIR}.zip"
7z x -y mongosh.zip > /dev/null
cp "${MONGOSH_DIR}/bin/mongosh.exe" bundle/
cp "${MONGOSH_DIR}/bin/mongosh_crypt_v1.dll" bundle/
# Windows launcher from the repo root.
cp src/start-wekan.bat bundle/
echo "=== bundle contents ==="
ls -1 bundle/
- name: Create win64 zip
shell: cmd
@@ -460,9 +455,62 @@ jobs:
path: wekan-${{ needs.prepare.outputs.version }}-win64.zip
retention-days: 1
# ─── 6. Create GitHub Release and upload all bundles ────────────────────────
# ─── 6. Build mac-arm64 bundle (rebuild native modules on Apple Silicon) ─────
build-mac-arm64:
needs: [prepare, build-amd64]
runs-on: macos-14 # macos-14 runners are Apple Silicon (arm64)
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for macOS arm64
run: |
unzip -q wekan-${VERSION}-amd64.zip
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
if [ -d "$BCRYPT_DIR" ]; then
rm -rf "$BCRYPT_DIR"
TMP=$(mktemp -d)
pushd "$TMP" > /dev/null
npm pack bcrypt@6.0.0
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
cd bcrypt && npm install --ignore-scripts && cd ..
cp -a bcrypt "$BCRYPT_DIR"
popd > /dev/null
rm -rf "$TMP"
fi
cd bundle/programs/server
rm -rf node_modules
npm install
cd ../../..
- name: Create mac-arm64 zip
run: zip -r wekan-${VERSION}-mac-arm64.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-mac-arm64
path: wekan-${{ needs.prepare.outputs.version }}-mac-arm64.zip
retention-days: 1
# ─── 7. Create GitHub Release and upload all bundles ────────────────────────
release:
needs: [prepare, build-amd64, build-arm64, build-s390x, build-ppc64le, build-win64]
needs:
- prepare
- build-amd64
- build-arm64
- build-win64
- build-mac-arm64
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
@@ -482,6 +530,104 @@ jobs:
files: |
wekan-${{ needs.prepare.outputs.version }}-amd64.zip
wekan-${{ needs.prepare.outputs.version }}-arm64.zip
wekan-${{ needs.prepare.outputs.version }}-s390x.zip
wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
wekan-${{ needs.prepare.outputs.version }}-win64.zip
wekan-${{ needs.prepare.outputs.version }}-mac-arm64.zip
# ─── 8. Build and push the multi-arch Docker image to all registries ────────
# Mirrors releases/docker-build.sh step 3 (multi-arch buildx --push).
# Needs `release` because the Dockerfile downloads wekan-<version>-<arch>.zip
# from the GitHub Release; building before those assets exist 404s and (without
# set -e in the Dockerfile) shipped a broken image missing /build/main.js.
docker:
needs: [prepare, release]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub, Quay.io and GHCR
env:
DOCKERHUB_AUTH: ${{ secrets.DOCKERHUB_AUTH }}
QUAY_AUTH: ${{ secrets.QUAY_AUTH }}
GHCR_AUTH: ${{ secrets.GHCR_AUTH }}
run: |
login() {
# $1 = registry host, $2 = base64("user:token")
local host="$1" creds user token
creds="$(printf '%s' "$2" | base64 -d)"
user="${creds%%:*}"
token="${creds#*:}"
printf '%s' "$token" | docker login "$host" -u "$user" --password-stdin
}
login docker.io "$DOCKERHUB_AUTH"
login quay.io "$QUAY_AUTH"
login ghcr.io "$GHCR_AUTH"
- name: Build and push multi-arch image
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t wekanteam/wekan:v${VERSION} \
-t wekanteam/wekan:latest \
-t quay.io/wekan/wekan:v${VERSION} \
-t quay.io/wekan/wekan:latest \
-t ghcr.io/wekan/wekan:v${VERSION} \
-t ghcr.io/wekan/wekan:latest \
--push \
.
# ─── 9. Build the snap on Launchpad, push to the store, attach to release ────
# The snap's `wekan` part downloads wekan-<version>-<arch>.zip from the GitHub
# Release, so this job runs only after `release` has published those assets.
# `snapcraft remote-build` builds every platform listed in snapcraft.yaml
# (amd64 + arm64) on Launchpad.
snap:
needs: [prepare, release]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
ref: refs/tags/v${{ needs.prepare.outputs.version }}
# remote-build (Launchpad) rejects shallow clones, so fetch full history
fetch-depth: 0
- name: Install snapcraft
run: sudo snap install snapcraft --classic
- name: Provide Launchpad credentials for remote-build
env:
LP_CREDENTIALS: ${{ secrets.LP_CREDENTIALS }}
run: |
mkdir -p ~/.local/share/snapcraft
printf '%s' "$LP_CREDENTIALS" | base64 -d > ~/.local/share/snapcraft/launchpad-credentials
chmod 600 ~/.local/share/snapcraft/launchpad-credentials
- name: Build snaps on Launchpad (amd64 + arm64)
run: |
snapcraft remote-build --launchpad-accept-public-upload
- name: Push snaps to the Snap Store (edge + beta channels)
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
run: |
for f in wekan_${VERSION}_*.snap; do
echo "Uploading $f to the Snap Store (edge,beta)..."
snapcraft upload --release=edge,beta "$f"
done
- name: Attach snaps to the GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "v${VERSION}" wekan_${VERSION}_*.snap --clobber
+7 -7
View File
@@ -16,11 +16,11 @@ blaze@3.0.2
aldeed:collection2
aldeed:schema-index@4.0.0-beta.5
reywood:publish-composite
mongo@2.4.0-beta350.12
mongo@2.4.0-rc350.1
# Account system
accounts-password@3.3.0-beta350.12
accounts-express
accounts-password@3.3.0-rc350.1
accounts-express@1.0.0-rc350.1
communitypackages:core
communitypackages:unstyled
communitypackages:flow-routing
@@ -37,7 +37,7 @@ wekan-accounts-oidc
check@1.5.0
random@1.2.2
reactive-dict@1.3.2
session@1.2.3-beta350.12
session@1.2.3-rc350.1
tracker@1.3.4
audit-argument-checks@1.0.8
@@ -45,7 +45,7 @@ audit-argument-checks@1.0.8
ostrio:i18n
reactive-var@1.0.13
shell-server@0.7.0
email@3.2.0-beta350.12
email@3.2.0-rc350.1
dynamic-import@0.7.4
wekan-markdown
quave:synced-cron
@@ -55,8 +55,8 @@ ostrio:files
meteortesting:mocha@3.0.0
matb33:collection-hooks
service-configuration@1.3.5
minifier-css@2.0.2-beta350.12
ejson@1.2.0-beta350.12
minifier-css@2.0.2-rc350.1
ejson@1.2.0-rc350.1
logging@1.3.6
wekan-fullcalendar
wekan-fontawesome
+1 -1
View File
@@ -1 +1 @@
METEOR@3.5-beta.12
METEOR@3.5-rc.1
+25 -25
View File
@@ -1,8 +1,8 @@
accounts-base@3.3.0-beta350.12
accounts-oauth@1.4.7-beta350.12
accounts-password@3.3.0-beta350.12
accounts-express@1.0.0-beta350.12
aldeed:collection2@4.1.5
accounts-base@3.3.0-rc350.1
accounts-express@1.0.0-rc350.1
accounts-oauth@1.4.7-rc350.1
accounts-password@3.3.0-rc350.1
aldeed:collection2@4.2.0
aldeed:schema-index@4.0.0
aldeed:simple-schema@1.13.1
allow-deny@2.1.0
@@ -10,35 +10,35 @@ audit-argument-checks@1.0.8
autoupdate@2.0.1
babel-compiler@7.14.0
babel-runtime@1.5.2
base64@1.0.14-beta350.12
binary-heap@1.0.13-beta350.12
base64@1.0.14-rc350.1
binary-heap@1.0.13-rc350.1
blaze@3.0.3
blaze-tools@2.0.1
boilerplate-generator@2.1.0
caching-compiler@2.0.1
caching-html-compiler@2.0.1
callback-hook@1.8.0-beta350.12
callback-hook@1.8.0-rc350.1
check@1.5.0
communitypackages:core@2.0.3
communitypackages:flow-routing@2.1.0
communitypackages:unstyled@2.0.1
core-runtime@1.0.0
ddp@1.4.2
ddp-client@3.3.0-beta350.12
ddp-common@1.4.5-beta350.12
ddp-rate-limiter@1.3.0-beta350.12
ddp-server@3.3.0-beta350.12
ddp-client@3.3.0-rc350.1
ddp-common@1.4.5-rc350.1
ddp-rate-limiter@1.3.0-rc350.1
ddp-server@3.3.0-rc350.1
diff-sequence@1.1.3
dynamic-import@0.7.4
ecmascript@0.18.0
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.13.0
ecmascript-runtime-server@0.11.1
ejson@1.2.0-beta350.12
email@3.2.0-beta350.12
ejson@1.2.0-rc350.1
email@3.2.0-rc350.1
es5-shim@4.8.1
facts-base@1.0.2
fetch@0.2.0-beta350.12
fetch@0.2.0-rc350.1
geojson-utils@1.0.12
hot-code-push@1.0.5
html-tools@2.0.1
@@ -53,23 +53,23 @@ lai:collection-extensions@1.0.0
localstorage@1.2.1
logging@1.3.6
matb33:collection-hooks@2.1.0
meteor@2.3.1-beta350.12
meteor@2.3.1-rc350.1
meteor-base@1.5.2
meteortesting:browser-tests@1.8.0
meteortesting:mocha@3.3.0
meteortesting:mocha-core@8.2.0
minifier-css@2.0.2-beta350.12
minifier-css@2.0.2-rc350.1
minifier-js@3.2.0
minimongo@2.2.0-beta350.12
minimongo@2.2.0-rc350.1
modern-browsers@0.2.3
modules@0.20.3
modules-runtime@0.13.2
mongo@2.4.0-beta350.12
mongo@2.4.0-rc350.1
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.9
npm-mongo@6.16.1
oauth@3.0.4-beta350.12
oauth@3.0.4-rc350.1
oauth2@1.3.3
observe-sequence@2.0.1
ordered-dict@1.2.0
@@ -82,7 +82,7 @@ promise@1.0.0
quave:synced-cron@2.4.0
raix:eventemitter@1.0.0
random@1.2.2
rate-limit@1.2.0-beta350.12
rate-limit@1.2.0-rc350.1
react-fast-refresh@0.3.0
reactive-dict@1.3.2
reactive-var@1.0.13
@@ -92,10 +92,10 @@ reywood:publish-composite@1.9.0
routepolicy@1.1.2
rspack@1.1.0
service-configuration@1.3.5
session@1.2.3-beta350.12
session@1.2.3-rc350.1
sha@1.0.10
shell-server@0.7.0
socket-stream-client@0.7.0-beta350.12
socket-stream-client@0.7.0-rc350.1
spacebars@2.0.1
spacebars-compiler@2.0.1
standard-minifier-js@3.2.0
@@ -106,8 +106,8 @@ templating-tools@2.0.1
tools-core@1.1.0
tracker@1.3.4
typescript@5.10.0
url@1.3.6-beta350.12
webapp@2.2.0-beta350.12
url@1.3.6-rc350.1
webapp@2.2.0-rc350.1
webapp-hashing@1.1.2
wekan-accounts-cas@0.2.0
wekan-accounts-lockout@1.1.0
+1038
View File
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -3,9 +3,14 @@
For all code at WeKan GitHub Organization https://github.com/wekan
- All code in pull requests need to have permission already to add it to WeKan with MIT license, and will become MIT license.
- All code xet7 add is MIT license.
- For any dependencies, permissive licenses like https://copyfree.org are preferred
- For anything currently that is non-permissive (like GPL, AGPL, SSPL), those will be replaced with permissive-licensed alternatives
- Any intelligence allowed: Human, AI or any other. No discrimination, please have world peace. Mentioning participants optional.
- All code is checked and validated by xet7 and many different security AI. If code is real working fix or feature, it usually will be merged.
- All known vulnerabilities have been fixed at newest WeKan, please upgrade https://wekan.fi/upgrade/
- There is no known backdoors. If WeKan is installed by someone else, first registered user is Admin and has Admin Panel access.
- Maintainer of WeKan has no access to someone elses installed WeKan.
- For someone elses installed WeKan, see [Forgot Password](https://github.com/wekan/wekan/blob/main/docs/Login/Forgot-Password.md)
# Reporting about violations or something else
+10 -4
View File
@@ -34,6 +34,16 @@ and click send PR.
## Contributing code
Use rebuild-wekan.sh to run all tests to verify you don't break any features, before sending PR.
Only send one feature or fix at one PR. Do not add many unrelated changes.
Only send those changed code lines, that are related to your one feature or fix.
Do not format unrelated code. Do not use code formatting or linting like eslist or prettier.
Send your code ASAP, because there are daily releases of WeKan with major features and fixes added.
[Building WeKan and sending PR](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md).
WeKan code contributors Hall of Fame is at ChangeLog, where
@@ -49,10 +59,6 @@ Also see other docs:
https://github.com/wekan/wekan/tree/main/docs/DeveloperDocs
Do not use code formatting or linting like eslist or prettier.
Only send minimal changed code lines, that are related to feature or fix.
WeKan code has MIT license.
About 300 persons have contributed to WeKan, stats at:
+8 -3
View File
@@ -7,7 +7,7 @@ LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=9.32
ARG VERSION=9.42
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
@@ -160,9 +160,14 @@ ENV \
RUN <<EOR
set -o xtrace
# Fail hard on any error so a missing release zip / failed download can never
# produce a "successful" image with an empty /build (Cannot find /build/main.js).
set -eo pipefail
# Create Wekan user
useradd --user-group --system --home-dir /home/wekan wekan
# Create Wekan user. --create-home is required because --system users do not
# get a home directory by default; without it /home/wekan never exists and the
# later `chown ... /home/wekan/` aborts the build (now that set -e is active).
useradd --user-group --system --create-home --home-dir /home/wekan wekan
# OS Updates
apt-get update --assume-yes
+26 -28
View File
@@ -7,9 +7,11 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
### 1. Security Researcher:
- **Report**: Please email details regarding security vulnerabilities to **security@wekan.fi**.
- **Report**: Click "New draft security advisory" at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
More info at [GitHub Docs about reporting privately](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/report-privately).
- **Proof of Concept (PoC)**: If possible, include a fix or a reproduction script/code.
- **Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
- **If GitHub does not work**: Please send report to **security@wekan.fi**
- **Optional Email Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
### 2. WeKan Security Team:
@@ -17,17 +19,14 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
Fixes are announced at the top of the [ChangeLog](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
- **Recognition**: We will acknowledge your contribution by adding you to our
[Hall of Fame](https://wekan.fi/hall-of-fame/).
- **CVE Policy**: WeKan Security does not facilitate **CVE Coordination** due to
our rapid release cycle (new versions are released almost daily).
- **CVE Policy**: WeKan Security requests CVE at GitHub when releasing Security Advisory.
### 3. Post-Release and Public Disclosure:
- **Independent CVE Assignment**: After the fix is public, you are welcome to coordinate
**CVE assignment** independently. Please send the assigned CVE IDs to **security@wekan.fi**,
and we will update your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/).
- Publicity: If you publish blog posts, give talks, or write articles about the
reported vulnerabilities, please send the links to us. We will link them
to your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/) for additional context.
- Advisories reported at GitHub are listed at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
## How Should Reports Be Formatted?
@@ -51,34 +50,34 @@ added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
## Which domains are in scope?
No public domains, because all those are donated to Wekan Open Source project,
No public domains, because all those are donated to WeKan Open Source project,
and we don't have any permissions to do security scans on those donated servers.
Please don't perform research that could impact other users. Second, please keep
the reports short and succinct. If we fail to understand the logic of your bug, we will tell you.
You can [Install Wekan](https://wekan.fi/install/) on your own computer
You can [Install WeKan](https://wekan.fi/install/) on your own computer
and scan it's vulnerabilities there.
## About Wekan Versions
## About WeKan Versions
There are 2 versions of Wekan: Standalone Wekan, and Sandstorm Wekan.
There are 2 versions of WeKan: Standalone WeKan, and Sandstorm WeKan.
## Meteor Security
https://guide.meteor.com/security
### Standalone Wekan Security
### Standalone WeKan Security
Standalone Wekan includes all non-Sandstorm platforms. Some Standalone Wekan platforms
Standalone WeKan includes all non-Sandstorm platforms. Some Standalone WeKan platforms
like Snap and Docker have their own specific sandboxing etc features.
Standalone Wekan by default does not load any files from Internet, like fonts, CSS, etc.
This also means all Standalone Wekan functionality works in offline local networks.
Standalone WeKan by default does not load any files from Internet, like fonts, CSS, etc.
This also means all Standalone WeKan functionality works in offline local networks.
WeKan is used in most countries of the world https://snapcraft.io/wekan
and by companies that have 30k users.
- Wekan private board attachments are not accessible without logging in.
- WeKan private board attachments are not accessible without logging in.
- There is feature to set board public, so that board is visible without logging
in readonly mode, with realtime updates.
- Admin Panel has feature to disable all public boards, so all boards are private.
@@ -96,12 +95,12 @@ and by companies that have 30k users.
- WeKan uses DOMPurify npm package to filter for XSS at fields like cards, as you can see from
[package.json](https://github.com/wekan/wekan/blob/main/package.json). Other used versions can be seen from
[Meteor versions file](https://github.com/wekan/wekan/blob/main/.meteor/versions).
- Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
- Forms can include markdown links, html, image tags etc like you see at https://wekan.fi .
- It's possible to add attachments to cards, and markdown/html links to files.
- DOMPurify cleans up viewed code, so JavaScript in input fields does not execute
- https://wekan.github.io/hall-of-fame/fieldbleed/
- https://wekan.fi/hall-of-fame/fieldbleed/
- Reaction in comment is now checked, that it does not have extra added code
- https://wekan.github.io/hall-of-fame/reactionbleed/
- https://wekan.fi/hall-of-fame/reactionbleed/
- https://github.com/wekan/wekan/blob/main/packages/markdown/src/template-integration.js#L76
## QA About PubSub
@@ -131,7 +130,7 @@ A:
PubSub/websocket data (for example with Burp Suite Community Edition), etc.
- If you have a REST API, also check that only those who have a login token and permission can view or edit text.
- You should not include any data user is not allowed to see. Not to webpage text, not to websockets/PubSub, etc.
- Minimongo should not have password hashes PubSub https://wekan.github.io/hall-of-fame/userbleed/
- Minimongo should not have password hashes PubSub https://wekan.fi/hall-of-fame/userbleed/
- PubSub uses WebSockets, so you need those to be enabled on the webserver like Caddy/Nginx/Apache etc, examples of settings
at right menu of https://github.com/wekan/wekan/wiki
- Clientside https://github.com/wekan/wekan/tree/main/client/components subscribes to
@@ -155,7 +154,7 @@ A:
- With PubSub, there is checking, that someone modifying Websockets content, like permission isAdmin, can not change to Admin.
- https://github.com/wekan/wekan/commit/cbad4cf5943d47b916f64b4582f8ca76a9dfd743
- https://wekan.github.io/hall-of-fame/adminbleed/
- https://wekan.fi/hall-of-fame/adminbleed/
## Permissions and Roles
@@ -191,12 +190,12 @@ Meteor.startup(() => {
- Someone reported that it was a problem that the content of HTML comments in edit mode was not visible in view mode, so this change makes HTML comments visible.
- https://github.com/wekan/wekan/commit/167863d95711249e69bb3511175d73b34acbbdb3
- https://wekan.github.io/hall-of-fame/invisiblebleed/
- https://wekan.fi/hall-of-fame/invisiblebleed/
## Attachments: XSS in filename is sanitized
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- https://wekan.github.io/hall-of-fame/filebleed/
- https://wekan.fi/hall-of-fame/filebleed/
### Attachments: Forced download to prevent stored XSS
@@ -254,7 +253,7 @@ Meteor.startup(() => {
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
- https://github.com/wekan/wekan/tree/main/packages/wekan-accounts-lockout
### Sandstorm Wekan Security
### Sandstorm WeKan Security
On the Sandstorm platform, Standalone WeKan features like the Admin Panel are turned off using environment variables, because Sandstorm platform provides SSO for all apps running on Sandstorm.
@@ -267,7 +266,7 @@ Also read [Sandstorm Security Practices](https://docs.sandstorm.io/en/latest/usi
[Sandstorm Security non-events](https://docs.sandstorm.io/en/latest/using/security-non-events/).
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
## What Wekan bugs are eligible?
## What WeKan bugs are eligible?
Any typical web security bugs. If any of the previously mentioned is somehow problematic and
a security issue, we'd like to know about it, and also how to fix it:
@@ -279,7 +278,7 @@ a security issue, we'd like to know about it, and also how to fix it:
- Authentication bypass
- Server-side code execution
## What Wekan bugs are NOT eligible?
## What WeKan bugs are NOT eligible?
Typical already-known or 'no impact' bugs such as:
@@ -287,5 +286,4 @@ Typical already-known or 'no impact' bugs such as:
- Denial of service
- SSL BEAST/CRIME/etc. WeKan does not have SSL built-in; it uses Caddy/Nginx/Apache on the front end.
Wekan is Open Source with MIT license, and free to use also for commercial use.
We welcome all fixes to improve security by email to security@wekan.fi
WeKan is Open Source with MIT license, and free to use also for commercial use.
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v9.32.0"
appVersion: "v9.42.0"
files:
userUploads:
- README.md
+454 -1
View File
@@ -71,7 +71,52 @@ If *nix: chmod +x api.py => ./api.py users
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
Board Member API (Issue #5998):
python3 api.py addboardmember BOARDID USERID ROLE # Add/activate board member. ROLE: admin, normal, comment, readonly, worker, normalassignedonly, commentassignedonly, readassignedonly, nocomments
python3 api.py removeboardmember BOARDID USERID # Remove (deactivate) board member
python3 api.py setboardmemberrole BOARDID USERID ROLE # Change permission/role of existing board member
Card Member / Assignee API (board member -> card member/assignee):
python3 api.py setcardmembers BOARDID LISTID CARDID MEMBERIDS # Set card members. MEMBERIDS = comma-separated userIds, or '' to clear
python3 api.py setcardassignees BOARDID LISTID CARDID ASSIGNEEIDS # Set card assignees. ASSIGNEEIDS = comma-separated userIds, or '' to clear
Card Dates API (Issue #5846):
python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE [DATEVALUE] # DATETYPE: received, start, due, end. DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z. Omit DATEVALUE (or pass '') to clear the date.
Card Labels API (Issue #5819):
python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS # Set (replace) card labels. LABELIDS = comma-separated labelIds, or '' to clear
Card Copy/Move API:
python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID # Move card to another board/swimlane/list
Bulk Card API (Issue #4743, #5819):
python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE # Create many cards in one request. CARDSJSONFILE = JSON file with a list of titles, or a list of card objects {title,description,members,assignees,...}
python3 api.py bulkdeletecards BOARDID CARDIDS # Delete many cards in one request. CARDIDS = comma-separated cardIds
python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS # Merge-add/remove labels across many cards. CARDIDS/ADDLABELIDS/REMOVELABELIDS = comma-separated, or '' for none
Linked Card API (Issue #5897):
python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID # Create a linked card in BOARDID/LISTID that references LINKEDCARDID (may be on another board)
Card Member/Assignee Merge API (Issue #5998):
python3 api.py addcardmember BOARDID LISTID CARDID MEMBERID # Add one board member to a card (validated)
python3 api.py removecardmember BOARDID LISTID CARDID MEMBERID # Remove one member from a card
python3 api.py addcardassignee BOARDID LISTID CARDID ASSIGNEEID # Add one assignee to a card (validated)
python3 api.py removecardassignee BOARDID LISTID CARDID ASSIGNEEID # Remove one assignee from a card
Copy/Move Swimlane/List/Card API (position = 0-based index from top-left):
python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION] # Copy a card (deep copy)
python3 api.py copyswimlane BOARDID SWIMLANEID [TOBOARDID] [POSITION] # Copy a swimlane (deep copy)
python3 api.py moveswimlane BOARDID SWIMLANEID [TOBOARDID] [POSITION] # Move a swimlane
python3 api.py copylist BOARDID LISTID TOSWIMLANEID [TOBOARDID] [POSITION] # Copy a list (deep copy)
python3 api.py movelist BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION] # Move a list
My Cards / Due Cards API (Issue #4815):
python3 api.py mycards [due] [FROM] [TO] # Current user's cards. 'due' = only cards with a due date; FROM/TO = ISO 8601 due-date range
Card Settings API (Issue #3062):
python3 api.py getcardsettings BOARDID # Get board-level card settings (allows* toggles)
python3 api.py setcardsetting BOARDID KEY VALUE # Set one board card setting, e.g. allowsDueDate true
Admin API:
python3 api.py newuser USERNAME EMAIL PASSWORD
"""
@@ -1035,3 +1080,411 @@ if arguments >= 1:
# ------- DELETE ATTACHMENT END -----------
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS START -----------
# These commands wrap REST endpoints that already exist in Wekan.
# See CHANGELOG.md "Upcoming" section for related issues and design notes.
# Map a human readable ROLE to the board member permission boolean flags
# used by /api/boards/:boardId/members/:userId/add and .../members/:memberId
def board_role_flags(role):
flags = {
'isAdmin': 'false',
'isNoComments': 'false',
'isCommentOnly': 'false',
'isWorker': 'false',
'isNormalAssignedOnly': 'false',
'isCommentAssignedOnly': 'false',
'isReadOnly': 'false',
'isReadAssignedOnly': 'false',
}
role = (role or 'normal').lower()
rolemap = {
'admin': 'isAdmin',
'nocomments': 'isNoComments',
'comment': 'isCommentOnly',
'commentonly': 'isCommentOnly',
'worker': 'isWorker',
'normalassignedonly': 'isNormalAssignedOnly',
'commentassignedonly': 'isCommentAssignedOnly',
'readonly': 'isReadOnly',
'readassignedonly': 'isReadAssignedOnly',
}
if role in ('normal', 'member'):
return flags # all false == Normal board member
if role in rolemap:
flags[rolemap[role]] = 'true'
return flags
print("Invalid ROLE. Valid: admin, normal, comment, readonly, worker, "
"normalassignedonly, commentassignedonly, readassignedonly, nocomments")
sys.exit(1)
if sys.argv[1] == 'addboardmember':
# ------- ADD BOARD MEMBER START -----------
if arguments < 3:
print("Usage: python3 api.py addboardmember BOARDID USERID ROLE")
exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
role = sys.argv[4] if arguments > 3 else 'normal'
add_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'add'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'action': 'add'}
post_data.update(board_role_flags(role))
body = requests.post(add_member_url, data=post_data, headers=headers)
print("=== ADD BOARD MEMBER ===\n")
print(body.text)
# ------- ADD BOARD MEMBER END -----------
if sys.argv[1] == 'removeboardmember':
# ------- REMOVE BOARD MEMBER START -----------
if arguments < 3:
print("Usage: python3 api.py removeboardmember BOARDID USERID")
exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
remove_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'remove'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'action': 'remove'}
body = requests.post(remove_member_url, data=post_data, headers=headers)
print("=== REMOVE BOARD MEMBER ===\n")
print(body.text)
# ------- REMOVE BOARD MEMBER END -----------
if sys.argv[1] == 'setboardmemberrole':
# ------- SET BOARD MEMBER ROLE START -----------
if arguments < 4:
print("Usage: python3 api.py setboardmemberrole BOARDID USERID ROLE")
exit(1)
boardid = sys.argv[2]
userid = sys.argv[3]
role = sys.argv[4]
set_role_url = wekanurl + apiboards + boardid + s + 'members' + s + userid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = board_role_flags(role)
body = requests.post(set_role_url, data=post_data, headers=headers)
print("=== SET BOARD MEMBER ROLE ===\n")
print(body.text)
# ------- SET BOARD MEMBER ROLE END -----------
if sys.argv[1] in ('setcardmembers', 'setcardassignees'):
# ------- SET CARD MEMBERS / ASSIGNEES START -----------
if arguments < 4:
print("Usage: python3 api.py {} BOARDID LISTID CARDID IDS".format(sys.argv[1]))
print("IDS = comma-separated userIds, or '' to clear")
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
ids_raw = sys.argv[5] if arguments > 4 else ''
ids = [x for x in ids_raw.split(',') if x] if ids_raw else []
field = 'members' if sys.argv[1] == 'setcardmembers' else 'assignees'
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
# Empty array clears the field; send as JSON so the array type is preserved.
put_data = {field: ids if ids else []}
body = requests.put(edcard, json=put_data, headers=headers)
print("=== SET CARD {} ===\n".format(field.upper()))
print(body.text)
# ------- SET CARD MEMBERS / ASSIGNEES END -----------
if sys.argv[1] == 'setcarddate':
# ------- SET CARD DATE START -----------
if arguments < 5:
print("Usage: python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE DATEVALUE")
print("DATETYPE: received, start, due, end")
print("DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z")
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
datetype = sys.argv[5].lower()
datevalue = sys.argv[6] if arguments > 5 else ''
datemap = {
'received': 'receivedAt',
'start': 'startAt',
'due': 'dueAt',
'end': 'endAt',
}
if datetype not in datemap:
print("Invalid DATETYPE. Valid: received, start, due, end")
sys.exit(1)
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {datemap[datetype]: datevalue}
body = requests.put(edcard, data=put_data, headers=headers)
print("=== SET CARD DATE {} ===\n".format(datetype.upper()))
print(body.text)
# ------- SET CARD DATE END -----------
if sys.argv[1] == 'setcardlabels':
# ------- SET CARD LABELS START -----------
if arguments < 4:
print("Usage: python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS")
print("LABELIDS = comma-separated labelIds, or '' to clear")
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
labelids_raw = sys.argv[5] if arguments > 4 else ''
labelids = [x for x in labelids_raw.split(',') if x] if labelids_raw else []
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
# NOTE: this REPLACES the card's labels. Bulk add/remove (merge) is not yet
# implemented server-side, see Issue #5819 in CHANGELOG.md "Upcoming".
put_data = {'labelIds': labelids if labelids else []}
body = requests.put(edcard, json=put_data, headers=headers)
print("=== SET CARD LABELS ===\n")
print(body.text)
# ------- SET CARD LABELS END -----------
if sys.argv[1] == 'movecard':
# ------- MOVE CARD START -----------
if arguments < 7:
print("Usage: python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID")
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
newboardid = sys.argv[5]
newswimlaneid = sys.argv[6]
newlistid = sys.argv[7]
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {
'newBoardId': newboardid,
'newSwimlaneId': newswimlaneid,
'newListId': newlistid,
}
body = requests.put(edcard, data=put_data, headers=headers)
print("=== MOVE CARD ===\n")
print(body.text)
# ------- MOVE CARD END -----------
if sys.argv[1] == 'bulkaddcards':
# ------- BULK ADD CARDS START -----------
if arguments < 5:
print("Usage: python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE")
print("CARDSJSONFILE = JSON file with a list of titles, e.g. [\"Card A\", \"Card B\"],")
print(" or a list of card objects [{\"title\": \"Card A\", \"description\": \"...\"}, ...]")
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
authorid = sys.argv[4]
swimlaneid = sys.argv[5]
cardsjsonfile = sys.argv[6] if arguments > 5 else None
if not cardsjsonfile:
print("CARDSJSONFILE is required")
exit(1)
try:
with open(cardsjsonfile, 'r') as f:
raw_cards = json.load(f)
except Exception as e:
print("Error reading CARDSJSONFILE: {}".format(e))
exit(1)
if not isinstance(raw_cards, list) or len(raw_cards) == 0:
print("CARDSJSONFILE must contain a non-empty JSON list")
exit(1)
cards = []
for item in raw_cards:
if isinstance(item, str):
cards.append({'title': item})
elif isinstance(item, dict):
cards.append(item)
else:
print("Each card must be a string (title) or an object")
exit(1)
bulk_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + 'bulk'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'cards': cards}
body = requests.post(bulk_url, json=post_data, headers=headers)
print("=== BULK ADD CARDS ===\n")
print(body.text)
# ------- BULK ADD CARDS END -----------
if sys.argv[1] == 'bulkdeletecards':
# ------- BULK DELETE CARDS START -----------
if arguments < 3:
print("Usage: python3 api.py bulkdeletecards BOARDID CARDIDS")
print("CARDIDS = comma-separated cardIds")
exit(1)
boardid = sys.argv[2]
cardids_raw = sys.argv[3]
cardids = [x for x in cardids_raw.split(',') if x]
bulk_url = wekanurl + apiboards + boardid + s + cs + s + 'bulk'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
# requests sends a body with DELETE via the json= parameter.
body = requests.delete(bulk_url, json={'cardIds': cardids}, headers=headers)
print("=== BULK DELETE CARDS ===\n")
print(body.text)
# ------- BULK DELETE CARDS END -----------
if sys.argv[1] == 'bulkcardlabels':
# ------- BULK CARD LABELS START -----------
if arguments < 5:
print("Usage: python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS")
print("CARDIDS / ADDLABELIDS / REMOVELABELIDS = comma-separated, or '' for none")
exit(1)
boardid = sys.argv[2]
cardids = [x for x in sys.argv[3].split(',') if x]
add_labelids = [x for x in sys.argv[4].split(',') if x]
remove_labelids = [x for x in sys.argv[5].split(',') if x]
bulk_url = wekanurl + apiboards + boardid + s + cs + s + 'labels'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
post_data = {'cardIds': cardids, 'addLabelIds': add_labelids, 'removeLabelIds': remove_labelids}
body = requests.post(bulk_url, json=post_data, headers=headers)
print("=== BULK CARD LABELS ===\n")
print(body.text)
# ------- BULK CARD LABELS END -----------
if sys.argv[1] == 'linkcard':
# ------- CREATE LINKED CARD START -----------
if arguments < 6:
print("Usage: python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID")
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
swimlaneid = sys.argv[4]
authorid = sys.argv[5]
linkedcardid = sys.argv[6]
link_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'linkedId': linkedcardid}
body = requests.post(link_url, data=post_data, headers=headers)
print("=== CREATE LINKED CARD ===\n")
print(body.text)
# ------- CREATE LINKED CARD END -----------
if sys.argv[1] in ('addcardmember', 'removecardmember', 'addcardassignee', 'removecardassignee'):
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE START -----------
if arguments < 5:
print("Usage: python3 api.py {} BOARDID LISTID CARDID USERID".format(sys.argv[1]))
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
userid = sys.argv[5]
field = 'members' if 'member' in sys.argv[1] else 'assignees'
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + field + s + userid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
if sys.argv[1].startswith('add'):
body = requests.post(url, headers=headers)
else:
body = requests.delete(url, headers=headers)
print("=== {} ===\n".format(sys.argv[1].upper()))
print(body.text)
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE END -----------
if sys.argv[1] == 'copycard':
# ------- COPY CARD START -----------
if arguments < 4:
print("Usage: python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION]")
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
toswimlaneid = sys.argv[5] if arguments > 4 else None
if not toswimlaneid:
print("TOSWIMLANEID is required")
exit(1)
toboardid = sys.argv[6] if arguments > 5 else boardid
tolistid = sys.argv[7] if arguments > 6 else listid
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + 'copy'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
post_data = {'toBoardId': toboardid, 'toSwimlaneId': toswimlaneid, 'toListId': tolistid}
if arguments > 7:
post_data['position'] = int(sys.argv[8])
body = requests.post(url, json=post_data, headers=headers)
print("=== COPY CARD ===\n")
print(body.text)
# ------- COPY CARD END -----------
if sys.argv[1] in ('copyswimlane', 'moveswimlane'):
# ------- COPY/MOVE SWIMLANE START -----------
if arguments < 3:
print("Usage: python3 api.py {} BOARDID SWIMLANEID [TOBOARDID] [POSITION]".format(sys.argv[1]))
exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
toboardid = sys.argv[4] if arguments > 3 else boardid
action = 'copy' if sys.argv[1] == 'copyswimlane' else 'move'
url = wekanurl + apiboards + boardid + s + sws + s + swimlaneid + s + action
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
post_data = {'toBoardId': toboardid}
if arguments > 4:
post_data['position'] = int(sys.argv[5])
body = requests.post(url, json=post_data, headers=headers)
print("=== {} ===\n".format(sys.argv[1].upper()))
print(body.text)
# ------- COPY/MOVE SWIMLANE END -----------
if sys.argv[1] in ('copylist', 'movelist'):
# ------- COPY/MOVE LIST START -----------
if arguments < 3:
print("Usage: python3 api.py {} BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION]".format(sys.argv[1]))
exit(1)
boardid = sys.argv[2]
listid = sys.argv[3]
toswimlaneid = sys.argv[4] if arguments > 3 else None
toboardid = sys.argv[5] if arguments > 4 else boardid
action = 'copy' if sys.argv[1] == 'copylist' else 'move'
url = wekanurl + apiboards + boardid + s + l + s + listid + s + action
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
post_data = {'toBoardId': toboardid}
if toswimlaneid:
post_data['toSwimlaneId'] = toswimlaneid
if arguments > 5:
post_data['position'] = int(sys.argv[6])
body = requests.post(url, json=post_data, headers=headers)
print("=== {} ===\n".format(sys.argv[1].upper()))
print(body.text)
# ------- COPY/MOVE LIST END -----------
if sys.argv[1] == 'mycards':
# ------- MY CARDS / DUE CARDS START -----------
params = {}
if arguments >= 2 and sys.argv[2].lower() == 'due':
params['due'] = 'true'
if arguments >= 3:
params['from'] = sys.argv[3]
if arguments >= 4:
params['to'] = sys.argv[4]
url = wekanurl + 'api/user/cards'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
body = requests.get(url, headers=headers, params=params)
print("=== MY CARDS ===\n")
data2 = body.text.replace('}', "}\n")
print(data2)
# ------- MY CARDS / DUE CARDS END -----------
if sys.argv[1] == 'getcardsettings':
# ------- GET CARD SETTINGS START -----------
if arguments < 2:
print("Usage: python3 api.py getcardsettings BOARDID")
exit(1)
boardid = sys.argv[2]
url = wekanurl + apiboards + boardid + s + 'cardSettings'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
body = requests.get(url, headers=headers)
print("=== CARD SETTINGS ===\n")
data2 = body.text.replace(',', ",\n")
print(data2)
# ------- GET CARD SETTINGS END -----------
if sys.argv[1] == 'setcardsetting':
# ------- SET CARD SETTING START -----------
if arguments < 4:
print("Usage: python3 api.py setcardsetting BOARDID KEY VALUE")
print("e.g. python3 api.py setcardsetting BOARDID allowsDueDate true")
exit(1)
boardid = sys.argv[2]
key = sys.argv[3]
value = sys.argv[4]
url = wekanurl + apiboards + boardid + s + 'cardSettings'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
body = requests.put(url, json={key: value}, headers=headers)
print("=== SET CARD SETTING ===\n")
print(body.text)
# ------- SET CARD SETTING END -----------
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS END -----------
+2 -2
View File
@@ -8,10 +8,10 @@ template(name="archivedBoards")
each archivedBoards
li.archived-lists-item
div.board-header-btns
button.board-header-btn.js-delete-board
button.primary.board-header-btn.js-delete-board
i.fa.fa-trash
| {{_ 'delete-board'}}
button.board-header-btn.js-restore-board
button.primary.board-header-btn.js-restore-board
i.fa.fa-undo
| {{_ 'restore-board'}}
= title
+1 -1
View File
@@ -64,7 +64,7 @@ template(name="boardBody")
template(name="calendarView")
if isViewCalendar
.calendar-view.swimlane
.calendar-view.swimlane.dragscroll
if currentCard
+cardDetails(currentCard)
+fullcalendar(calendarOptions)
+35 -57
View File
@@ -2,6 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import '/client/lib/dragscrollTouch';
import { boardConverter } from '/client/lib/boardConverter';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import Swimlanes from '/models/swimlanes';
@@ -19,23 +20,26 @@ let BoardBody = null;
Template.board.onCreated(function () {
this.isBoardReady = new ReactiveVar(false);
// Kept for the template/jade which still references it; conversion is a no-op.
this.isConverting = new ReactiveVar(false);
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
this._boardProcessed = false; // Track if board has been processed
this._lastProcessedBoardId = null; // Track last processed board ID
this._swimlaneCreated = new Set(); // boards where a default swimlane was ensured
// Ensure a board has at least one swimlane. Guarded to run once per board and
// fire-and-forget so it never blocks rendering. The board is marked
// immediately to avoid duplicate concurrent calls, and the mark is rolled
// back on failure so a later attempt can retry.
this.ensureDefaultSwimlane = async (boardId) => {
// Only create swimlane once per board
if (this._swimlaneCreated.has(boardId)) {
return;
}
this._swimlaneCreated.add(boardId);
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
if (!board) {
this._swimlaneCreated.delete(boardId);
return;
}
if (board.swimlanes().length === 0) {
const swimlaneId = await Meteor.callAsync('ensureDefaultSwimlane', boardId);
if (process.env.DEBUG === 'true' && swimlaneId) {
console.log(
@@ -43,67 +47,41 @@ Template.board.onCreated(function () {
);
}
}
this._swimlaneCreated.add(boardId);
} catch (error) {
this._swimlaneCreated.delete(boardId); // allow a later retry
console.error('Error creating default swimlane:', error);
}
};
this.checkAndConvertBoard = async (boardId) => {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) {
this.isBoardReady.set(true);
return;
}
this.isBoardReady.set(true);
} catch (error) {
console.error('Error during board conversion check:', error);
this.isConverting.set(false);
this.isBoardReady.set(true); // Show board even if conversion check failed
}
};
// The pattern we use to manually handle data loading is described here:
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
// XXX The boardId should be readed from some sort the component "props",
// unfortunatly, Blaze doesn't have this notion.
// Single reactive computation that subscribes to the current board and tracks
// its ready state in one place. Previously a Tracker.autorun was created
// *inside* this autorun on every re-run without being stopped, leaking
// computations that raced on shared ready-state flags. When navigating
// between boards — e.g. opening a freshly created board — that race could
// leave swimlanes/lists unrendered until the board was re-selected. A single
// self-managed autorun (auto-stopped by Blaze on destroy) avoids both the
// leak and the race.
// Pattern: https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
this.autorun(() => {
const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return;
if (!currentBoardId) {
this.isBoardReady.set(false);
return;
}
const handle = Meteor.subscribe('board', currentBoardId, false);
const ready = handle.ready();
this.isBoardReady.set(ready);
// Use a separate autorun for subscription ready state to avoid reactive loops
this.subscriptionReadyAutorun = Tracker.autorun(() => {
if (handle.ready()) {
if (
!this._boardProcessed ||
this._lastProcessedBoardId !== currentBoardId
) {
this._boardProcessed = true;
this._lastProcessedBoardId = currentBoardId;
// Ensure default swimlane exists (only once per board)
this.ensureDefaultSwimlane(currentBoardId);
// Check if board needs conversion
this.checkAndConvertBoard(currentBoardId);
}
} else {
this.isBoardReady.set(false);
}
});
if (ready) {
// Run outside the computation so this autorun depends only on the board
// id and the subscription's ready state — not on the swimlane data that
// ensureDefaultSwimlane reads — which would otherwise cause extra re-runs.
Tracker.nonreactive(() => this.ensureDefaultSwimlane(currentBoardId));
}
});
});
Template.board.onDestroyed(function () {
// Clean up the subscription ready autorun to prevent memory leaks
if (this.subscriptionReadyAutorun) {
this.subscriptionReadyAutorun.stop();
}
});
Template.board.helpers({
onlyShowCurrentCard() {
const isMiniScreen = Utils.isMiniScreen();
+16
View File
@@ -961,6 +961,22 @@ body.grey-icons-enabled .checkmark-no-grey {
font-size: inherit !important; /* Inherit the 2x scaling */
}
/* Stack the two-column All Boards layout on small screens. The fixed
"260px 1fr" grid had no mobile collapse, so on narrow non-iPhone browsers
(Fairphone Firefox ESR / Ubuntu Touch Morph, narrow desktop) it overflowed
and the left menu (Starred / Templates / Remaining / Workspaces / + Add
board) was clipped. Width-based, so it applies to every browser. */
.boards-layout {
grid-template-columns: 1fr !important;
}
.boards-left-menu {
border-right: none !important;
border-bottom: 1px solid #e0e0e0;
padding-right: 0 !important;
padding-bottom: 12px;
}
.wrapper .fa, .wrapper .icon {
font-size: 2em !important; /* 2x bigger icons */
}
+3 -3
View File
@@ -95,7 +95,7 @@ template(name="boardList")
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
ul.board-list.clearfix.js-boards.dragscroll(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
@@ -130,7 +130,7 @@ template(name="boardList")
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{boardWorkspaceDragHint}}")
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
span.emoji-icon
i.fa.fa-arrows
@@ -158,7 +158,7 @@ template(name="boardList")
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{boardWorkspaceDragHint}}")
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
span.emoji-icon
i.fa.fa-arrows
+9
View File
@@ -2,6 +2,8 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import getSlug from 'limax';
import dragscroll from '@wekanteam/dragscroll';
import '/client/lib/dragscrollTouch';
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
import { BoardMultiSelection } from '/client/lib/boardMultiSelection';
import { EscapeActions } from '/client/lib/escapeActions';
@@ -197,6 +199,13 @@ Template.boardList.onCreated(function () {
});
Template.boardList.onRendered(function () {
// Enable drag-to-scroll on the All Boards list (.board-list.dragscroll), the
// same way the board canvas/swimlanes do. This makes mouse drag-scrolling
// work on desktop; finger drag-scrolling is handled by dragscrollTouch.js.
// The .board-list element is stable across menu/workspace switches (only its
// li children are re-rendered), so a single reset() binding is enough.
dragscroll.reset();
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
// The old sortable code has been removed to prevent conflicts
/* OLD SORTABLE CODE - DISABLED
@@ -55,11 +55,10 @@ template(name="cardCustomField-number")
template(name="cardCustomField-checkbox")
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
if canModifyCard
span.check-box-unicode
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
.check-box-container
.check-box.materialCheckBox(class="{{#if data.value}}is-checked{{/if}}")
else
span.check-box-unicode
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
.materialCheckBox(class="{{#if data.value}}is-checked{{/if}}")
template(name="cardCustomField-currency")
if canModifyCard
@@ -106,9 +106,6 @@ Template['cardCustomField-checkbox'].onCreated(function () {
});
Template['cardCustomField-checkbox'].events({
'click .js-checklist-item .check-box-unicode'(event, tpl) {
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
},
'click .js-checklist-item .check-box-container'(event, tpl) {
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
},
+164
View File
@@ -890,3 +890,167 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
transform: scale(1.01);
transition: all 0.2s ease;
}
/* Sticker picker + card-detail stickers + due-complete toggle */
.card-stickers-picker {
max-width: 320px;
}
.card-sticker-select {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
margin: 2px;
border-radius: 3px;
font-size: 18px;
color: #4d4d4d;
cursor: pointer;
position: relative;
}
.card-sticker-select:hover {
background: #e0e0e0;
}
.card-sticker-select.active {
background: #d6ecd2;
color: #3cb500;
}
.card-sticker-select .card-sticker-check {
position: absolute;
bottom: 1px;
right: 2px;
font-size: 10px;
color: #3cb500;
}
.card-stickers .card-sticker {
display: inline-flex;
align-items: center;
margin: 0 8px 4px 0;
font-size: 18px;
}
.card-sticker .card-sticker-remove {
margin-left: 3px;
font-size: 11px;
color: #999;
cursor: pointer;
}
.card-sticker .card-sticker-remove:hover {
color: #eb4646;
}
.card-due-complete-toggle {
display: inline-flex;
align-items: center;
margin-left: 8px;
vertical-align: middle;
}
/* Trello-style "mark complete" toggle, left of the card title */
.card-details-complete-toggle {
float: left;
display: flex;
align-items: center;
/* Title is 1.35em with 1.3 line-height (~28px box) and margin-top 7px; push
the 13px checkbox down so it lands at the vertical middle of the title. */
margin: 14px 10px 0 0;
cursor: pointer;
}
/* Card locations: multiple locations, each editable with a remove button */
.card-details-item-location .card-locations {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 6px;
}
.card-details-item-location .card-location {
display: flex;
align-items: flex-start;
justify-content: space-between;
background: #fff;
border: 1px solid #e3e3e3;
border-radius: 4px;
padding: 6px 8px;
}
.card-details-item-location .card-location-info {
flex: 1;
}
.card-details-item-location .js-edit-location {
cursor: pointer;
}
.card-details-item-location .card-location-name {
font-weight: bold;
}
.card-details-item-location .card-location-address {
color: #4d4d4d;
}
.card-details-item-location .card-location-remove {
margin-left: 8px;
color: #8c8c8c;
cursor: pointer;
}
.card-details-item-location .card-location-remove:hover {
color: #eb4646;
}
/* Sticker icon highlight styles (used instead of colour): taco pack => underline,
pete pack => round (ring around the icon). Icon keeps its normal colour. */
.card-sticker i.sticker-underline {
border-bottom: 2px solid currentColor;
padding-bottom: 1px;
line-height: 1;
}
.card-sticker i.sticker-round {
border: 1px solid currentColor;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.6em;
height: 1.6em;
line-height: 1;
box-sizing: border-box;
}
/* "Open map links at" provider selector in the location popup */
.js-card-location-form .map-provider-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.js-card-location-form .map-provider-row .js-map-provider {
flex: 0 0 auto;
width: auto;
}
/* Opened card (card details popup) docks to the very top of the window and
overlays the global + board header bars instead of opening below them. The
base .pop-over is position:absolute with a small margin/top offset, which left
the card's top behind the fixed header bars; fix it to the viewport top with a
z-index above every header (headers top out around z-index 1000-2000). */
.pop-over[data-popup="cardDetailsPopup"] {
position: fixed !important;
top: 0 !important;
margin-top: 0 !important;
z-index: 100000 !important;
}
/* Sticker highlight styles inside the sticker picker (mascot = underline,
computer = round), matching how they render on the card. */
.card-sticker-select i.sticker-underline {
border-bottom: 2px solid currentColor;
padding-bottom: 1px;
line-height: 1;
}
.card-sticker-select i.sticker-round {
border: 1px solid currentColor;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.6em;
height: 1.6em;
line-height: 1;
box-sizing: border-box;
}
+80
View File
@@ -61,6 +61,9 @@ template(name="cardDetails")
span.emoji-icon
i.fa.fa-link
span.copied-tooltip {{_ 'copied'}}
if canModifyCard
a.card-details-complete-toggle.js-toggle-due-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
+viewer
@@ -126,6 +129,47 @@ template(name="cardDetails")
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
i.fa.fa-plus
.card-details-item.card-details-item-stickers
h3.card-details-item-title
i.fa.fa-sticky-note-o
| {{_ 'stickers'}}
.card-details-item-content.card-stickers
each stickers
span.card-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
if canModifyCard
a.card-sticker-remove.js-remove-sticker(data-index="{{@index}}" title="{{_ 'remove-sticker'}}")
i.fa.fa-times
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-add-stickers(title="{{_ 'add-sticker'}}")
i.fa.fa-plus
.card-details-item.card-details-item-location
h3.card-details-item-title
i.fa.fa-map-marker
| {{_ 'location'}}
.card-details-item-content.card-locations
each getLocations
.card-location
.card-location-info(class="{{#if canModifyCard}}js-edit-location{{/if}}" data-location-id="{{_id}}")
if name
.card-location-name= name
if address
.card-location-address= address
if hasCoordinates
a.card-location-map(href=mapUrl target="_blank" rel="noopener noreferrer")
i.fa.fa-map-o
= ' '
| {{_ 'location-open-map'}}
if canModifyCard
a.card-location-remove.js-remove-location(data-location-id="{{_id}}" title="{{_ 'delete'}}")
i.fa.fa-times
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-add-location(title="{{_ 'add-location'}}")
i.fa.fa-plus
if currentBoard.hasAnyAllowsDate
hr
@@ -1196,3 +1240,39 @@ template(name="cardStartPlanningPokerPopup")
if getPokerQuestion
if currentUser.isBoardAdmin
button.js-remove-poker.negate.wide.right {{_ 'delete'}}
template(name="cardLocationsPopup")
form.js-card-location-form
label
| {{_ 'location-detect-from-map'}}
input.js-location-map-link(type="text" placeholder="https://..." autofocus)
button.primary.js-detect-location(type="button") {{_ 'location-detect'}}
if detectMessage
p.quiet.js-detect-message {{detectMessage}}
hr
label
| {{_ 'location-name'}}
input.js-location-name(type="text" value="{{location.name}}")
label
| {{_ 'location-address'}}
input.js-location-address(type="text" value="{{location.address}}")
label
| {{_ 'location-latitude'}}
input.js-location-latitude(type="number" step="any" value="{{location.latitude}}")
label
| {{_ 'location-longitude'}}
input.js-location-longitude(type="number" step="any" value="{{location.longitude}}")
button.primary.confirm.js-submit-location(type="submit") {{_ 'save'}}
if location._id
button.negate.wide.right.js-delete-location(type="button") {{_ 'delete'}}
hr
.map-provider-row
span.map-provider-label {{_ 'location-open-map-at'}}
select.js-map-provider
option(value="openstreetmap" selected="{{#if isMapProvider 'openstreetmap'}}selected{{/if}}") OpenStreetMap
option(value="google" selected="{{#if isMapProvider 'google'}}selected{{/if}}") Google Maps
option(value="bing" selected="{{#if isMapProvider 'bing'}}selected{{/if}}") Bing Maps
option(value="apple" selected="{{#if isMapProvider 'apple'}}selected{{/if}}") Apple Maps
button.primary.js-save-map-provider(type="button") {{_ 'save'}}
if mapSavedMessage
p.quiet.js-map-saved {{mapSavedMessage}}
+275
View File
@@ -51,8 +51,100 @@ import { UnsavedEdits } from '/client/lib/unsavedEdits';
import { EscapeActions } from '/client/lib/escapeActions';
import { MultiSelection } from '/client/lib/multiSelection';
import { Utils } from '/client/lib/utils';
import { ReactiveVar } from 'meteor/reactive-var';
import autosize from 'autosize';
// Id of the location currently being edited in the cardLocationsPopup; null
// when adding a new location.
const editingLocationId = new ReactiveVar(null);
// Parse coordinates (and, when present, a place name / address) out of a map
// link from common providers: Google Maps, OpenStreetMap, Bing Maps, Apple Maps
// and generic `?q=lat,lon` / `?ll=lat,lon` links. Returns whatever it can find
// as { latitude, longitude, name, address }.
function parseMapLink(url) {
const result = {};
if (!url) return result;
const s = String(url).trim();
let lat;
let lon;
const setCoords = (la, lo) => {
if (lat !== undefined) return;
const a = parseFloat(la);
const o = parseFloat(lo);
if (!isNaN(a) && !isNaN(o) && Math.abs(a) <= 90 && Math.abs(o) <= 180) {
lat = a;
lon = o;
}
};
const N = '(-?\\d+(?:\\.\\d+)?)';
let m;
// Google Maps place marker in the data part: !3d<lat>!4d<lon> (most precise).
if ((m = s.match(new RegExp(`!3d${N}!4d${N}`)))) setCoords(m[1], m[2]);
// Google Maps view/place: @<lat>,<lon>
if ((m = s.match(new RegExp(`@${N},${N}`)))) setCoords(m[1], m[2]);
// OpenStreetMap marker: ?mlat=..&mlon=..
if ((m = s.match(new RegExp(`[?&#]mlat=${N}`)))) {
const mlon = s.match(new RegExp(`[?&#]mlon=${N}`));
if (mlon) setCoords(m[1], mlon[1]);
}
// OpenStreetMap map hash: #map=z/lat/lon
if ((m = s.match(new RegExp(`[#&]map=\\d+(?:\\.\\d+)?/${N}/${N}`)))) setCoords(m[1], m[2]);
// Bing Maps: cp=lat~lon
if ((m = s.match(new RegExp(`[?&]cp=${N}~${N}`)))) setCoords(m[1], m[2]);
// Apple Maps and generic ll=lat,lon
if ((m = s.match(new RegExp(`[?&]ll=${N},${N}`)))) setCoords(m[1], m[2]);
// Generic q=lat,lon (Google/Apple query form)
if ((m = s.match(new RegExp(`[?&]q=${N},${N}`)))) setCoords(m[1], m[2]);
// Generic center=lat,lon
if ((m = s.match(new RegExp(`[?&]center=${N},${N}`)))) setCoords(m[1], m[2]);
if (lat !== undefined) {
result.latitude = lat;
result.longitude = lon;
}
const decode = (raw) => {
try {
return decodeURIComponent(raw.replace(/\+/g, ' ')).trim();
} catch (e) {
return raw.replace(/\+/g, ' ').trim();
}
};
const isCoordText = (t) => /^-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?$/.test(t);
// Place name from Google Maps /place/<Name>/
if ((m = s.match(/\/place\/([^/@?]+)/))) {
const name = decode(m[1]);
if (name && !isCoordText(name)) result.name = name;
}
// Address from a textual q=/query=/destination= parameter.
if ((m = s.match(/[?&](?:q|query|destination)=([^&]+)/))) {
const q = decode(m[1]);
if (q && !isCoordText(q)) result.address = q;
}
return result;
}
// Build an "open in map" link for the given provider and coordinates. Mirrors
// the providers offered in the location popup's "Open map links at" selector.
function mapLinkFor(provider, lat, lon) {
switch (provider) {
case 'google':
return `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`;
case 'bing':
return `https://www.bing.com/maps?cp=${lat}~${lon}&lvl=16`;
case 'apple':
return `https://maps.apple.com/?ll=${lat},${lon}`;
case 'openstreetmap':
default:
return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`;
}
}
// SubsManager removed for Meteor 3 migration
const { calculateIndexData } = Utils;
@@ -121,9 +213,24 @@ Template.cardDetails.onCreated(function () {
};
Meteor.subscribe('unsaved-edits');
// Surface legacy CollectionFS attachments for the current board so they show
// up in the card's attachment gallery and can be read in place (without
// migrating them to Meteor-Files first).
this.autorun(() => {
const board = Utils.getCurrentBoard();
if (board && board._id) {
Meteor.subscribe('legacyBoardAttachments', board._id);
}
});
});
Template.cardDetails.onRendered(function () {
// A reactive re-render (e.g. a card moving between the inline swimlane render
// and the draggable openCards popup) can create and then remove this instance
// within the same flush. If our DOM range is already gone, calling this.$()
// below throws "Can't select in removed DomRange", so bail out early.
if (this.view && this.view.isDestroyed) return;
this.calculateNextPeak();
if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
// Send Webhook but not create Activities records ---
@@ -263,6 +370,23 @@ Template.cardDetails.helpers({
return card.findWatcher(Meteor.userId());
},
// Returns the card's locations (multiple supported), each enriched with the
// coordinate flag and OpenStreetMap link used by the template.
getLocations() {
const card = Template.currentData();
if (!card || !card.getLocations) return [];
const user = ReactiveCache.getCurrentUser();
const provider = user ? user.getMapProvider() : 'openstreetmap';
return card.getLocations().map(loc => {
const hasCoordinates =
typeof loc.latitude === 'number' && typeof loc.longitude === 'number';
const mapUrl = hasCoordinates
? mapLinkFor(provider, loc.latitude, loc.longitude)
: '';
return { ...loc, hasCoordinates, mapUrl };
});
},
customFieldsGrid() {
return ReactiveCache.getCurrentUser().hasCustomFieldsGrid();
},
@@ -600,9 +724,57 @@ Template.cardDetails.events({
'click .js-assignee': Popup.open('cardAssignee'),
'click .js-add-assignees': Popup.open('cardAssignees'),
'click .js-add-labels': Popup.open('cardLabels'),
'click .js-add-stickers': Popup.open('cardStickers'),
'click .js-remove-sticker'(event) {
event.preventDefault();
event.stopPropagation();
if (!Utils.canModifyCard()) return;
const index = parseInt(event.currentTarget.dataset.index, 10);
const card = Template.currentData();
if (card && !Number.isNaN(index)) {
card.removeStickerAt(index);
}
},
'click .js-add-location'(event) {
event.preventDefault();
if (!Utils.canModifyCard()) return;
const card = Template.currentData();
editingLocationId.set(null);
Popup.open('cardLocations')(event, {
dataContextIfCurrentDataIsUndefined: card,
});
},
'click .js-edit-location'(event) {
// Let the "Open in map" link work without also opening the edit popup.
if ($(event.target).closest('a.card-location-map').length) return;
event.preventDefault();
if (!Utils.canModifyCard()) return;
const card = Template.currentData();
editingLocationId.set(event.currentTarget.dataset.locationId || null);
Popup.open('cardLocations')(event, {
dataContextIfCurrentDataIsUndefined: card,
});
},
'click .js-remove-location'(event) {
event.preventDefault();
event.stopPropagation();
if (!Utils.canModifyCard()) return;
const locationId = event.currentTarget.dataset.locationId;
const card = Template.currentData();
if (card && locationId) {
card.removeLocation(locationId);
}
},
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-start-date': Popup.open('editCardStartDate'),
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-toggle-due-complete'(event) {
event.preventDefault();
event.stopPropagation();
if (!Utils.canModifyCard()) return;
const card = Template.currentData();
card.setDueComplete(!card.getDueComplete());
},
'click .js-end-date': Popup.open('editCardEndDate'),
'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
@@ -1134,6 +1306,109 @@ Template.cardMembersPopup.helpers({
},
});
// Popup that adds or edits a single card location (name, address, latitude,
// longitude). Cards can hold multiple locations, like members.
Template.cardLocationsPopup.onCreated(function () {
const data = Template.currentData();
this.cardId = data && data._id;
this.detectMsg = new ReactiveVar('');
this.mapSavedMsg = new ReactiveVar('');
});
Template.cardLocationsPopup.helpers({
location() {
const tpl = Template.instance();
const card = ReactiveCache.getCard(tpl.cardId);
const id = editingLocationId.get();
if (card && id) {
const found = card.getLocations().find(loc => loc._id === id);
if (found) return found;
}
return {};
},
detectMessage() {
return Template.instance().detectMsg.get();
},
isMapProvider(provider) {
const user = ReactiveCache.getCurrentUser();
const current = user ? user.getMapProvider() : 'openstreetmap';
return current === provider;
},
mapSavedMessage() {
return Template.instance().mapSavedMsg.get();
},
});
Template.cardLocationsPopup.events({
'click .js-detect-location'(event) {
event.preventDefault();
const tpl = Template.instance();
const linkInput = tpl.find('.js-location-map-link');
const parsed = parseMapLink(linkInput ? linkInput.value : '');
let filled = false;
if (typeof parsed.latitude === 'number') {
tpl.find('.js-location-latitude').value = parsed.latitude;
tpl.find('.js-location-longitude').value = parsed.longitude;
filled = true;
}
if (parsed.name) {
tpl.find('.js-location-name').value = parsed.name;
filled = true;
}
if (parsed.address) {
tpl.find('.js-location-address').value = parsed.address;
filled = true;
}
tpl.detectMsg.set(
TAPi18n.__(filled ? 'location-detect-done' : 'location-detect-none'),
);
},
'submit .js-card-location-form'(event) {
event.preventDefault();
const tpl = Template.instance();
const card = ReactiveCache.getCard(tpl.cardId);
if (!card) {
Popup.back();
return;
}
const name = tpl.find('.js-location-name').value.trim();
const address = tpl.find('.js-location-address').value.trim();
const latRaw = tpl.find('.js-location-latitude').value.trim();
const lonRaw = tpl.find('.js-location-longitude').value.trim();
const latitude = latRaw === '' ? undefined : parseFloat(latRaw);
const longitude = lonRaw === '' ? undefined : parseFloat(lonRaw);
const data = { name, address, latitude, longitude };
const id = editingLocationId.get();
if (id) {
card.updateLocation(id, data);
} else {
card.addLocation(data);
}
Popup.back();
},
'click .js-delete-location'(event) {
event.preventDefault();
const tpl = Template.instance();
const card = ReactiveCache.getCard(tpl.cardId);
const id = editingLocationId.get();
if (card && id) {
card.removeLocation(id);
}
Popup.back();
},
'click .js-save-map-provider'(event) {
event.preventDefault();
const tpl = Template.instance();
const select = tpl.find('.js-map-provider');
const provider = select ? select.value : 'openstreetmap';
Meteor.call('setMapProvider', provider, err => {
tpl.mapSavedMsg.set(
TAPi18n.__(err ? 'server-error' : 'map-provider-saved'),
);
});
},
});
const filterMembers = (filterTerm) => {
let currBoard = Utils.getCurrentBoard();
let members = currBoard.activeMembers();
@@ -0,0 +1,7 @@
template(name="cardStickersPopup")
.card-stickers-picker.clearfix
each stickerIcons
a.card-sticker-select.js-select-sticker(class="{{#if isSelected}}active{{/if}}" title="{{stickerTitle}}")
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
if isSelected
i.fa.fa-check.card-sticker-check
+39
View File
@@ -0,0 +1,39 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { STICKER_PICKER } from '/models/metadata/stickers';
// Picker popup that lets a user add/remove stickers on a card. Each entry is
// { icon, highlight?, name? }: the plain Font Awesome icons plus the mascot
// (underlined) and computer (ringed) highlighted stickers. Opened from the card
// details stickers section with the card as the data context.
Template.cardStickersPopup.onCreated(function () {
const data = Template.currentData();
this.cardId = data && data._id;
});
function pickerCard(tpl) {
return ReactiveCache.getCard(tpl.cardId);
}
Template.cardStickersPopup.helpers({
stickerIcons() {
return STICKER_PICKER;
},
isSelected() {
const card = pickerCard(Template.instance());
return !!(card && card.hasSticker(this.icon, this.highlight));
},
stickerTitle() {
return this.name || this.icon;
},
});
Template.cardStickersPopup.events({
'click .js-select-sticker'(event) {
event.preventDefault();
const card = pickerCard(Template.instance());
if (card) {
card.toggleSticker(this.icon, this.highlight, this.name);
}
},
});
+44
View File
@@ -28,6 +28,18 @@
.minicard-wrapper .multi-selection-checkbox + .minicard {
margin-left: 1vw;
}
/* Enlarge the clickable area of the small selection checkbox so it can be
reliably clicked at the left side of the minicard. The visible 13px box (and
its rotated is-checked checkmark) keep their size; an invisible overlay only
extends the hit target. */
.minicard-wrapper .multi-selection-checkbox::before {
content: "";
position: absolute;
top: -8px;
left: -8px;
right: -8px;
bottom: -8px;
}
@media only screen {
.minicard {
padding: 8px 1vw 3px;
@@ -822,3 +834,35 @@
word-wrap: break-word;
overflow-wrap: break-word;
}
/* Trello-style "mark complete" toggle, left of the minicard title */
.minicard-title .minicard-complete-toggle {
display: inline-flex;
align-items: center;
vertical-align: middle;
/* Nudge down so the checkmark sits at the vertical middle of the title text
(the checked materialCheckBox is shifted up 4px by its own positioning). */
position: relative;
top: 4px;
margin-right: 6px;
cursor: pointer;
}
/* Sticker icon highlight styles (used instead of colour): taco pack => underline,
pete pack => round (ring around the icon). Icon keeps its normal colour. */
.minicard-sticker i.sticker-underline {
border-bottom: 2px solid currentColor;
padding-bottom: 1px;
line-height: 1;
}
.minicard-sticker i.sticker-round {
border: 1px solid currentColor;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.6em;
height: 1.6em;
line-height: 1;
box-sizing: border-box;
}
+9 -1
View File
@@ -21,7 +21,7 @@ template(name="minicard")
+minicardStartDate
if showDue
if getDue
.date
.date(class="{{#if getDueComplete}}due-complete{{/if}}")
+minicardDueDate
if showEnd
if getEnd
@@ -54,6 +54,9 @@ template(name="minicard")
span {{_ 'upload-completed'}}
.minicard-title
if canModifyCard
a.minicard-complete-toggle.js-toggle-card-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
.parent-prefix
| {{ parentString ' > ' }}
@@ -152,6 +155,11 @@ template(name="minicard")
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
.badges
if stickers.length
.badge.badge-state-image-only.minicard-stickers
each stickers
span.badge-icon.minicard-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text
+10
View File
@@ -178,6 +178,16 @@ Template.minicard.events({
else if (this.isLinkedBoard())
Utils.goBoardId(this.linkedId);
},
'click .js-toggle-card-complete'(event) {
// Trello-style "mark complete" toggle (left of the title). Don't let the
// click open the card.
event.preventDefault();
event.stopPropagation();
if (!Utils.canModifyCard()) {
return;
}
this.setDueComplete(!this.getDueComplete());
},
'click .js-toggle-minicard-label-text'() {
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
@@ -0,0 +1,3 @@
.minicard-title .minicard-complete-toggle {
display: none;
}
+3
View File
@@ -139,6 +139,9 @@ textarea.js-edit-subtask-item {
.subtasks-item:hover {
background-color: #ebebeb;
}
.subtasks-item .check-box-container {
padding-right: 10px;
}
.subtasks-item .check-box {
margin: 0.1em 0 0 0;
}
+3 -4
View File
@@ -74,14 +74,13 @@ template(name="subtasksItems")
template(name='subtaskItemDetail')
.js-subtasks-item.subtasks-item
if canModifyCard
span.check-box-unicode
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
.check-box-container
.check-box.materialCheckBox(class="{{#if item.isFinished}}is-checked{{/if}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
else
span.check-box-unicode
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
.materialCheckBox(class="{{#if item.isFinished}}is-checked{{/if}}")
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
+1 -1
View File
@@ -119,7 +119,7 @@ Template.subtasks.helpers({
});
Template.subtaskItemDetail.events({
async 'click .js-subtasks-item .check-box-unicode'() {
async 'click .js-subtasks-item .check-box-container'() {
const item = Template.currentData().item;
if (item && item._id) {
await item.toggleItem();
+116
View File
@@ -49,3 +49,119 @@ a.show-mapping {
font-size: 90%;
font-weight: bold;
}
/* Trello API import: file rows, workspace tree, job progress and errors */
/* The shared page spinners are centered with a large top margin (e.g. bounce
uses margin:100px auto), which leaves a big empty gap above the spinner in
this compact panel. Make spinners here small and inline instead. */
.trello-api-import .sk-spinner {
margin: 0 8px 0 0;
display: inline-block;
vertical-align: middle;
width: auto;
}
.trello-api-import .import-file-row label {
display: block;
font-weight: bold;
}
.trello-api-import .import-file-row input,
.trello-api-import .import-file-row select {
width: 100%;
}
.trello-workspaces {
max-height: 40vh;
overflow-y: auto;
border: 1px solid #ddd;
padding: 8px;
margin: 8px 0;
}
.trello-workspace {
margin-bottom: 8px;
}
.trello-workspace .trello-boards {
margin-left: 22px;
}
.trello-workspace-title,
.trello-board {
align-items: center;
padding: 3px 0;
cursor: pointer;
text-decoration: none;
color: inherit;
}
.trello-workspace-title .materialCheckBox,
.trello-board .materialCheckBox {
flex: 0 0 auto;
margin-right: 12px;
}
.trello-select-controls button {
margin-right: 6px;
}
.trello-job-controls button {
margin-right: 6px;
}
/* Import progress meter, styled like Admin Panel / Attachments / Move Attachments */
.trello-progress-label {
margin: 8px 0 6px;
}
.trello-progress-bar {
background: #e9ecef;
border-radius: 8px;
height: 12px;
overflow: hidden;
margin-bottom: 4px;
}
.trello-progress-fill {
background: linear-gradient(90deg, #28a745, #20c997);
height: 100%;
border-radius: 8px;
transition: width 0.3s ease;
position: relative;
}
.trello-progress-fill.is-running::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: trelloProgressShimmer 2s infinite;
}
@keyframes trelloProgressShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.trello-progress-percent {
text-align: right;
font-size: 12px;
color: #666;
font-weight: 600;
margin-bottom: 8px;
}
.trello-import-result.is-ok {
color: #3cb500;
}
.trello-import-result.is-error {
color: #eb4646;
}
.js-trello-errors-text {
width: 100%;
font-family: monospace;
white-space: pre;
}
/* Trello API credential Save/Delete buttons */
.trello-api-creds-buttons button {
margin-right: 6px;
}
.trello-creds-saved {
color: #3cb500;
}
/* Simplified per-field hint text under the Trello file upload labels */
.import-file-row .import-file-hint {
display: block;
font-size: 0.85em;
margin: 2px 0 4px;
}
+110
View File
@@ -8,6 +8,8 @@ template(name="import")
.wrapper
if error.get
.warning {{_ error.get}}
if zipImporting
.warning {{_ 'import-trello-zip-progress'}}
+Template.dynamic(template=currentTemplate)
template(name="importTextarea")
@@ -15,7 +17,115 @@ template(name="importTextarea")
p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
textarea.js-import-json(id='import-textarea' placeholder="{{_ importPlaceHolder}}" autofocus)
| {{jsonText}}
if isTrelloImport
p.import-file-row
label(for='import-json-file') {{_ 'import-trello-json-file'}}
span.quiet.import-file-hint {{_ 'import-trello-json-file-hint'}}
input.js-import-json-file(id='import-json-file' type="file" accept=".json,application/json")
p.import-file-row
label(for='import-zip-file') {{_ 'import-trello-zip-file'}}
span.quiet.import-file-hint {{_ 'import-trello-zip-file-hint'}}
input.js-import-zip-file(id='import-zip-file' type="file" accept=".zip,application/zip")
p.import-file-row
label(for='import-workspace-name') {{_ 'import-trello-workspace'}}
input.js-import-workspace-name(id='import-workspace-name' type="text" placeholder="{{_ 'import-trello-workspace-placeholder'}}")
input.primary.wide(type="submit" value="{{_ 'import'}}")
if isTrelloImport
hr
+importTrelloApi
template(name="importTrelloApi")
.trello-api-import
h2 {{_ 'trello-api-import'}}
p.quiet {{_ 'trello-api-import-desc'}}
p.import-file-row
label(for='trello-api-key') {{_ 'trello-api-key'}}
input.js-trello-key(id='trello-api-key' type="text" autocomplete="off")
p.import-file-row
label(for='trello-api-token') {{_ 'trello-api-token'}}
input.js-trello-token(id='trello-api-token' type="password" autocomplete="off")
p.trello-api-creds-buttons
button.primary.js-trello-save-creds(type="button") {{_ 'save'}}
button.negate.js-trello-delete-creds(type="button") {{_ 'delete'}}
if credsSaved
p.quiet.trello-creds-saved
i.fa.fa-check
span {{_ 'trello-api-credentials-saved'}}
p
button.primary.js-trello-list-workspaces(type="button") {{_ 'trello-list-workspaces'}}
if error.get
.warning {{_ error.get}}
if loading.get
+spinner
if hasWorkspaces
p.import-file-row
label(for='trello-parent-workspace') {{_ 'import-trello-parent-workspace'}}
select.js-trello-parent-workspace(id='trello-parent-workspace')
option(value="") {{_ 'trello-parent-workspace-top'}}
each flatWorkspaceNodes
option(value=id)= label
p.trello-select-controls
button.primary.js-trello-select-all(type="button") {{_ 'select-all'}}
button.primary.js-trello-unselect-all(type="button") {{_ 'unselect-all'}}
.trello-workspaces
each workspaceList
.trello-workspace
a.flex.trello-workspace-title.js-toggle-workspace
.materialCheckBox(class="{{#if workspaceSelected}}is-checked{{/if}}")
b {{name}}
.trello-boards
each boards
a.flex.trello-board.js-toggle-board(data-board-id="{{id}}")
.materialCheckBox(class="{{#if boardSelected}}is-checked{{/if}}")
span {{name}}
if closed
span.quiet ({{_ 'archived'}})
if canStartImport
p
button.primary.wide.js-trello-import-selected(type="button") {{_ 'trello-import-selected'}}
if currentJob
hr
.trello-import-job
h3 {{_ 'trello-import-progress'}}
with currentJob
p.trello-progress-label
b {{_ 'status'}}:
span {{_ status}} ({{jobProgressText}})
.trello-progress-bar
.trello-progress-fill(class="{{#if $eq status 'running'}}is-running{{/if}}" style="width: {{jobProgressPercent}}%")
.trello-progress-percent {{jobProgressPercent}}%
if lastError
.warning {{lastError}}
p.trello-job-controls
if jobIsRunning
button.primary.js-trello-cancel(type="button") {{_ 'trello-cancel'}}
button.negate.js-trello-cancel-delete(type="button") {{_ 'trello-cancel-delete'}}
if jobCanResume
button.primary.js-trello-resume(type="button") {{_ 'trello-resume'}}
button.negate.js-trello-cancel-delete(type="button") {{_ 'trello-cancel-delete'}}
if jobIsFinished
button.negate.js-trello-cancel-delete(type="button") {{_ 'trello-delete-imported'}}
button.primary.js-trello-clear(type="button") {{_ 'trello-clear-job'}}
if jobResults.length
.trello-import-results
each jobResults
p.trello-import-result(class="{{#if success}}is-ok{{else}}is-error{{/if}}")
if success
i.fa.fa-check
span {{title}} — {{attachmentsImported}} {{_ 'attachments'}}
else
i.fa.fa-warning
span {{trelloBoardId}}: {{this.error}}
if jobHasErrors
.trello-import-errors
p
b {{_ 'trello-import-errors'}}
button.primary.js-trello-copy-errors(type="button") {{_ 'copy-to-clipboard'}}
if copied.get
span.quiet {{_ 'copied'}}
textarea.js-trello-errors-text(readonly rows="8" wrap="off")= jobErrorText
template(name="importMapMembers")
h2 {{_ 'import-map-members'}}
+515 -24
View File
@@ -6,6 +6,8 @@ import { csvGetMembersToMap } from './csvMembersMapper';
import getSlug from 'limax';
import { UserSearchIndex } from '/models/users';
import { Utils } from '/client/lib/utils';
import { TAPi18n } from '/imports/i18n';
import TrelloImportJobs from '/models/trelloImportJobs';
const Papa = require('papaparse');
@@ -44,6 +46,92 @@ function _prepareAdditionalData(dataObject) {
return membersToMap;
}
// All Trello file/text imports are run on the server over HTTP instead of the
// DDP `importBoard` method. This matters for correctness, not just size: Meteor
// automatically re-sends an unacknowledged method call on every reconnect, so if
// an import is heavy (or the connection hiccups) the WebSocket can enter an
// endless drop/retry loop (the "Invalid frame header" flicker). An HTTP request
// is never auto-retried, and a .zip's attachment bytes never touch the realtime
// connection at all. The body is either a .zip File (application/zip) or a JSON
// string { board, membersMapping } (application/json).
async function postTrelloImport(body, contentType) {
const token =
(window.localStorage && window.localStorage.getItem('Meteor.loginToken')) || '';
const resp = await fetch('/import-trello', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': contentType,
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body,
});
let result = {};
try {
result = await resp.json();
} catch (e) {
result = {};
}
if (!resp.ok || result.error) {
throw new Error(result.error || 'import-trello-failed');
}
return result;
}
// A safe URL slug for an imported board. Trello exports name the board `name`,
// WeKan exports use `title`; getSlug (limax) throws on undefined, so guard it.
function boardSlug(data) {
const raw = (data && (data.title || data.name)) || '';
return (raw && getSlug(raw)) || 'imported-board';
}
// Navigate to a freshly server-imported board. The HTTP import (unlike a DDP
// method) does not push the new board's documents to this client, so we first
// subscribe to the board and wait until its lists/swimlanes/cards are loaded
// into Minimongo — otherwise the board opens with an empty Swimlanes view until
// the page is reloaded. The subscription is left running so the data stays
// available when the board is reopened from All Boards in the same session. A
// timeout is the safety net in case the subscription never signals ready.
function goToImportedBoard(boardId, slug) {
let navigated = false;
const go = () => {
if (navigated) return;
navigated = true;
FlowRouter.go('board', { id: boardId, slug });
};
Meteor.subscribe('board', boardId, false, { onReady: go });
Meteor.setTimeout(go, 5000);
}
// Find a workspace node by name anywhere in the user's personal workspace tree.
function findWorkspaceByName(nodes, name) {
for (const node of nodes || []) {
if (node.name === name) return node;
if (node.children) {
const found = findWorkspaceByName(node.children, name);
if (found) return found;
}
}
return null;
}
// Assign an imported board to a personal workspace named `wsName`, creating the
// workspace (under an optional parent) only if one with that name doesn't exist.
function assignBoardToNamedWorkspace(boardId, wsName, parentId = null) {
const user = ReactiveCache.getCurrentUser();
const tree = (user && user.profile && user.profile.boardWorkspacesTree) || [];
const existing = findWorkspaceByName(tree, wsName);
if (existing) {
Meteor.call('assignBoardToWorkspace', boardId, existing.id);
return;
}
Meteor.call('createWorkspace', { parentId, name: wsName }, (err, node) => {
if (!err && node) {
Meteor.call('assignBoardToWorkspace', boardId, node.id);
}
});
}
Template.import.onCreated(function () {
this.error = new ReactiveVar('');
this.steps = ['importTextarea', 'importMapMembers'];
@@ -51,6 +139,8 @@ Template.import.onCreated(function () {
this.importedData = new ReactiveVar();
this.membersToMap = new ReactiveVar([]);
this.importSource = Session.get('importSource');
// True while a Trello .zip package is being uploaded/imported server-side.
this.zipImporting = new ReactiveVar(false);
this.nextStep = () => {
const nextStepIndex = this._currentStepIndex.get() + 1;
@@ -65,10 +155,10 @@ Template.import.onCreated(function () {
this.error.set(error);
};
this.importData = (evt, dataSource) => {
this.importData = async (evt, dataSource) => {
evt.preventDefault();
const input = this.find('.js-import-json').value;
if (dataSource === 'csv') {
const input = this.find('.js-import-json').value;
const csv = input.indexOf('\t') > 0 ? input.replace(/(\t)/g, ',') : input;
const ret = Papa.parse(csv);
if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
@@ -76,52 +166,136 @@ Template.import.onCreated(function () {
const membersToMap = _prepareAdditionalData(ret.data);
this.membersToMap.set(membersToMap);
this.nextStep();
} else {
try {
const dataObject = JSON.parse(input);
this.setError('');
this.importedData.set(dataObject);
const membersToMap = _prepareAdditionalData(dataObject);
// store members data and mapping in Session
// (we go deep and 2-way, so storing in data context is not a viable option)
this.membersToMap.set(membersToMap);
this.nextStep();
} catch (e) {
this.setError('error-json-malformed');
return;
}
// Trello: a .zip package (one or more board .json files plus per-board
// attachment subdirectories from the Trello Attachments Downloader) imports
// all of its boards at once, through a separate field from the single .json.
if (dataSource === 'trello') {
const zipEl = this.find('.js-import-zip-file');
if (zipEl && zipEl.files && zipEl.files[0]) {
await this.importTrelloZip(zipEl.files[0]);
return;
}
}
try {
// A single board: JSON may come from an uploaded .json file (large Trello
// exports are awkward to paste) or from the textarea.
let input = this.find('.js-import-json').value;
const jsonFileEl = this.find('.js-import-json-file');
if (jsonFileEl && jsonFileEl.files && jsonFileEl.files[0]) {
input = await jsonFileEl.files[0].text();
}
const dataObject = JSON.parse(input);
this.setError('');
// Trello: remember the target personal-workspace name for finishImport.
this.workspaceName = '';
if (dataSource === 'trello') {
const wsEl = this.find('.js-import-workspace-name');
this.workspaceName = wsEl && wsEl.value ? wsEl.value.trim() : '';
}
this.importedData.set(dataObject);
const membersToMap = _prepareAdditionalData(dataObject);
// store members data and mapping in Session
// (we go deep and 2-way, so storing in data context is not a viable option)
this.membersToMap.set(membersToMap);
this.nextStep();
} catch (e) {
this.setError('error-json-malformed');
}
};
this.finishImport = () => {
const additionalData = {};
// Upload a Trello .zip package to the server, which extracts it (with
// zip-bomb / path-traversal guards), imports every board and streams each
// attachment to the Default storage, then go to All Boards.
this.importTrelloZip = async (zipFile) => {
this.setError('');
const wsEl = this.find('.js-import-workspace-name');
const workspaceName = wsEl && wsEl.value ? wsEl.value.trim() : '';
this.zipImporting.set(true);
let result;
try {
result = await postTrelloImport(zipFile, 'application/zip');
} catch (e) {
this.zipImporting.set(false);
this.setError((e && e.message) || 'import-trello-failed');
return;
}
this.zipImporting.set(false);
(result.boardIds || []).forEach(boardId => {
if (workspaceName) {
assignBoardToNamedWorkspace(boardId, workspaceName);
}
});
Session.set('fromBoard', null);
// Go to All Boards, where the newly imported boards appear.
FlowRouter.go('home');
};
this.finishImport = async () => {
const membersMapping = this.membersToMap.get();
const mappingById = {};
if (membersMapping) {
const mappingById = {};
membersMapping.forEach(member => {
if (member.wekanId) {
mappingById[member.id] = member.wekanId;
}
});
additionalData.membersMapping = mappingById;
}
const importedData = this.importedData.get();
// Trello: import over HTTP (see postTrelloImport) so the realtime DDP
// connection is never used for the board payload and can't enter the
// drop/retry "Invalid frame header" flicker loop. Do NOT mutate the still-
// mounted map-members template (e.g. clearing membersToMap) before
// navigating away — that forces an empty re-render mid-teardown and can
// throw "Can't select in removed DomRange". We navigate away, which
// destroys the import templates.
if (this.importSource === 'trello') {
let result;
try {
result = await postTrelloImport(
JSON.stringify({ board: importedData, membersMapping: mappingById }),
'application/json',
);
} catch (e) {
this.setError((e && e.message) || 'import-trello-failed');
return;
}
const boardId = (result.boardIds || [])[0];
if (!boardId) {
this.setError('import-trello-failed');
return;
}
Session.set('fromBoard', null);
if (this.workspaceName) {
assignBoardToNamedWorkspace(boardId, this.workspaceName);
}
goToImportedBoard(boardId, boardSlug(importedData));
return;
}
// wekan / csv: unchanged DDP import.
this.membersToMap.set([]);
Meteor.call(
'importBoard',
this.importedData.get(),
additionalData,
importedData,
{ membersMapping: mappingById },
this.importSource,
Session.get('fromBoard'),
(err, res) => {
if (err) {
this.setError(err.error);
} else {
let title = getSlug(this.importedData.get().title) || 'imported-board';
Session.set('fromBoard', null);
FlowRouter.go('board', {
id: res,
slug: title,
slug: boardSlug(importedData),
});
//Utils.goBoardId(res);
}
},
);
@@ -135,6 +309,9 @@ Template.import.helpers({
currentTemplate() {
return Template.instance().steps[Template.instance()._currentStepIndex.get()];
},
zipImporting() {
return Template.instance().zipImporting.get();
},
});
Template.importTextarea.helpers({
@@ -149,6 +326,9 @@ Template.importTextarea.helpers({
return 'import-json-placeholder';
}
},
isTrelloImport() {
return Session.get('importSource') === 'trello';
},
});
Template.importTextarea.events({
@@ -320,7 +500,11 @@ Template.importMapMembersAddPopup.onCreated(function () {
});
Template.importMapMembersAddPopup.onRendered(function () {
this.find('.js-search-member-input').focus();
// Guard against the DOM range being gone (e.g. the popup was closed during a
// re-render) — calling find/$ then throws "Can't select in removed DomRange".
if (this.view && this.view.isDestroyed) return;
const input = this.find('.js-search-member-input');
if (input) input.focus();
});
Template.importMapMembersAddPopup.onDestroyed(function () {
@@ -380,3 +564,310 @@ Template.importMapMembersAddPopup.helpers({
return importMemberPopupState.noResults;
},
});
// ---------------------------------------------------------------------------
// Live Trello API import: key/token -> list workspaces & boards -> import
// selected boards (with attachments) server-side, placing each under a
// personal workspace named after its Trello workspace.
// ---------------------------------------------------------------------------
function flattenWorkspaceTree(nodes, depth = 0, acc = []) {
(nodes || []).forEach(node => {
acc.push({ id: node.id, label: `${'— '.repeat(depth)}${node.name}` });
if (node.children && node.children.length) {
flattenWorkspaceTree(node.children, depth + 1, acc);
}
});
return acc;
}
// Build the copy-paste-friendly error text for a job: the error log plus a
// summary line per failed board.
function jobErrorText(job) {
if (!job) return '';
const lines = [];
(job.errorLog || []).forEach(line => lines.push(line));
return lines.join('\n');
}
Template.importTrelloApi.onCreated(function () {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
this.workspaces = new ReactiveVar([]);
this.copied = new ReactiveVar(false);
// Board selection state: map of trelloBoardId -> true. Tracked here (rather
// than via DOM checkboxes) because the UI uses animated .materialCheckBox
// elements, not native inputs.
this.selectedBoards = new ReactiveVar({});
// The import itself runs server-side as a persisted job; watch it reactively
// so progress survives navigating away and back.
this.subscribe('trelloImportJobs');
});
// Collect every board id across all listed workspaces.
function allBoardIds(workspaces) {
const ids = {};
(workspaces || []).forEach(ws => {
(ws.boards || []).forEach(b => {
ids[b.id] = true;
});
});
return ids;
}
Template.importTrelloApi.helpers({
error() {
return Template.instance().error;
},
loading() {
return Template.instance().loading;
},
copied() {
return Template.instance().copied;
},
hasWorkspaces() {
return Template.instance().workspaces.get().length > 0;
},
workspaceList() {
return Template.instance().workspaces.get();
},
boardSelected() {
return !!Template.instance().selectedBoards.get()[this.id];
},
workspaceSelected() {
const sel = Template.instance().selectedBoards.get();
const boards = this.boards || [];
return boards.length > 0 && boards.every(b => sel[b.id]);
},
flatWorkspaceNodes() {
const user = ReactiveCache.getCurrentUser();
const tree = (user && user.profile && user.profile.boardWorkspacesTree) || [];
return flattenWorkspaceTree(tree);
},
credsSaved() {
const user = ReactiveCache.getCurrentUser();
return !!(user && user.profile && user.profile.trelloApiSaved);
},
// --- current background job ---
currentJob() {
return TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
},
jobIsRunning() {
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
return job && job.status === 'running';
},
jobCanResume() {
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
return job && (job.status === 'paused' || job.status === 'error');
},
jobIsFinished() {
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
return job && (job.status === 'done' || job.status === 'cancelled');
},
canStartImport() {
// Don't start a second import while one is active (running/paused/error),
// which would create a hidden concurrent job.
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
return !job || job.status === 'done' || job.status === 'cancelled';
},
jobProgressText() {
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
if (!job) return '';
return `${job.currentIndex} / ${job.total}`;
},
jobProgressPercent() {
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
if (!job || !job.total) return 0;
return Math.round((job.currentIndex / job.total) * 100);
},
jobResults() {
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
return (job && job.results) || [];
},
jobHasErrors() {
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
return !!(job && job.errorLog && job.errorLog.length);
},
jobErrorText() {
return jobErrorText(TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } }));
},
});
Template.importTrelloApi.events({
'click .js-trello-save-creds'(evt, tpl) {
evt.preventDefault();
const key = tpl.find('.js-trello-key').value.trim();
const token = tpl.find('.js-trello-token').value.trim();
if (!key || !token) {
tpl.error.set('trello-api-credentials-required');
return;
}
tpl.error.set('');
Meteor.call('saveTrelloCredentials', key, token, err => {
if (err) {
tpl.error.set(err.reason || err.error || 'trello-api-error');
return;
}
// Don't keep the token sitting in the browser; it now lives server-side.
tpl.find('.js-trello-key').value = '';
tpl.find('.js-trello-token').value = '';
});
},
'click .js-trello-delete-creds'(evt, tpl) {
evt.preventDefault();
tpl.error.set('');
Meteor.call('deleteTrelloCredentials', err => {
if (err) tpl.error.set(err.reason || err.error || 'trello-api-error');
});
tpl.find('.js-trello-key').value = '';
tpl.find('.js-trello-token').value = '';
},
'click .js-trello-list-workspaces'(evt, tpl) {
evt.preventDefault();
// key/token may be empty when saved credentials exist; the server falls
// back to the saved ones and returns an error if neither is available.
const key = tpl.find('.js-trello-key').value.trim();
const token = tpl.find('.js-trello-token').value.trim();
tpl.error.set('');
tpl.loading.set(true);
Meteor.call('trelloListWorkspaces', key, token, (err, res) => {
tpl.loading.set(false);
if (err) {
tpl.error.set(err.reason || err.error || 'trello-api-error');
tpl.workspaces.set([]);
tpl.selectedBoards.set({});
} else {
const workspaces = res || [];
tpl.workspaces.set(workspaces);
// Preselect all boards by default.
tpl.selectedBoards.set(allBoardIds(workspaces));
}
});
},
// Toggle a single board's animated checkbox.
'click .js-toggle-board'(evt, tpl) {
evt.preventDefault();
const id = this.id;
const sel = { ...tpl.selectedBoards.get() };
if (sel[id]) {
delete sel[id];
} else {
sel[id] = true;
}
tpl.selectedBoards.set(sel);
},
// Toggle all boards in a workspace: if all are selected, clear them; else
// select them all.
'click .js-toggle-workspace'(evt, tpl) {
evt.preventDefault();
const boards = this.boards || [];
const sel = { ...tpl.selectedBoards.get() };
const allSelected = boards.length > 0 && boards.every(b => sel[b.id]);
boards.forEach(b => {
if (allSelected) {
delete sel[b.id];
} else {
sel[b.id] = true;
}
});
tpl.selectedBoards.set(sel);
},
'click .js-trello-select-all'(evt, tpl) {
evt.preventDefault();
tpl.selectedBoards.set(allBoardIds(tpl.workspaces.get()));
},
'click .js-trello-unselect-all'(evt, tpl) {
evt.preventDefault();
tpl.selectedBoards.set({});
},
'click .js-trello-import-selected'(evt, tpl) {
evt.preventDefault();
const key = tpl.find('.js-trello-key').value.trim();
const token = tpl.find('.js-trello-token').value.trim();
const sel = tpl.selectedBoards.get();
const boardIds = Object.keys(sel).filter(id => sel[id]);
if (!boardIds.length) {
tpl.error.set('trello-select-boards');
return;
}
const parentEl = tpl.find('.js-trello-parent-workspace');
const parentId = parentEl && parentEl.value ? parentEl.value : null;
tpl.error.set('');
// Start the server-side job; progress shows up via the subscription. The
// user is free to navigate away and come back.
Meteor.call('trelloStartImport', key, token, boardIds, parentId, err => {
if (err) {
tpl.error.set(err.reason || err.error || 'trello-api-error');
}
});
},
'click .js-trello-resume'(evt, tpl) {
evt.preventDefault();
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
if (!job) return;
const key = tpl.find('.js-trello-key').value.trim();
const token = tpl.find('.js-trello-token').value.trim();
if (!key || !token) {
tpl.error.set('trello-api-credentials-required');
return;
}
tpl.error.set('');
Meteor.call('trelloResumeImport', job._id, key, token, err => {
if (err) tpl.error.set(err.reason || err.error || 'trello-api-error');
});
},
'click .js-trello-cancel'(evt, tpl) {
evt.preventDefault();
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
if (!job) return;
Meteor.call('trelloCancelImport', job._id, false);
},
'click .js-trello-cancel-delete'(evt, tpl) {
evt.preventDefault();
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
if (!job) return;
// eslint-disable-next-line no-alert
if (!window.confirm(TAPi18n.__('trello-cancel-delete-confirm'))) return;
Meteor.call('trelloCancelImport', job._id, true);
},
'click .js-trello-clear'(evt, tpl) {
evt.preventDefault();
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
if (!job) return;
Meteor.call('trelloClearImportJob', job._id, false);
},
'click .js-trello-copy-errors'(evt, tpl) {
evt.preventDefault();
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
const text = jobErrorText(job);
if (!text) return;
const done = () => {
tpl.copied.set(true);
setTimeout(() => tpl.copied.set(false), 2000);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, () => {
// Fall back to selecting the textarea so the user can copy manually.
const ta = tpl.find('.js-trello-errors-text');
if (ta) {
ta.focus();
ta.select();
}
});
} else {
const ta = tpl.find('.js-trello-errors-text');
if (ta) {
ta.focus();
ta.select();
try {
document.execCommand('copy');
done();
} catch (e) {
// user can copy manually from the selected textarea
}
}
}
},
});
+13 -3
View File
@@ -4,15 +4,24 @@ export function wekanGetMembersToMap(data) {
// we will work on the list itself (an ordered array of objects) when a
// mapping is done, we add a 'wekan' field to the object representing the
// imported member
const membersToMap = data.members;
const users = data.users;
const membersToMap = data.members || [];
const users = data.users || [];
// auto-map based on username
const mappable = [];
membersToMap.forEach(importedMember => {
importedMember.id = importedMember.userId;
delete importedMember.userId;
const user = users.filter(user => {
return user._id === importedMember.id;
})[0];
// The exported `users` array only contains users that still exist in the
// source instance, so a member whose account was deleted (or any other
// dangling user reference, e.g. from an older export) has no matching
// user. Skip it instead of dereferencing `undefined`, which would throw
// and make the whole import fail as "error-json-malformed".
if (!user) {
return;
}
if (user.profile && user.profile.fullname) {
importedMember.fullName = user.profile.fullname;
}
@@ -21,6 +30,7 @@ export function wekanGetMembersToMap(data) {
if (wekanUser) {
importedMember.wekanId = wekanUser._id;
}
mappable.push(importedMember);
});
return membersToMap;
return mappable;
}
+9
View File
@@ -38,6 +38,15 @@ Template.list.onRendered(function () {
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
// Destroy any existing sortable before re-initializing. Without this, a
// re-render of the list (e.g. when a Card Details panel opens) binds a SECOND
// sortable to the same .js-minicards element, so dragging a card from its
// handle fires the `stop` handler twice and the card appears to duplicate.
// Mirrors the guard in swimlanes.js initSortable().
if ($cards.data('uiSortable') || $cards.data('sortable')) {
$cards.sortable('destroy');
}
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
+25 -12
View File
@@ -194,6 +194,19 @@ Template.listBody.onCreated(function () {
const clickedLinkedReference = $target.closest('.js-linked-link').length > 0;
const clickedChecklist = $target.closest('.minicard-checklist').length > 0;
// When multi-selection is active (or Shift is held), a click anywhere on the
// minicard toggles its selection. This must be checked before the title and
// checklist handlers below, otherwise clicking a card's title (which covers
// most of the minicard) would open the card details instead of (de)selecting
// it, making it impossible to check some cards in multi-selection mode.
if (MultiSelection.isActive() || evt.shiftKey) {
evt.stopImmediatePropagation();
evt.preventDefault();
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
MultiSelection[methodName](card._id);
return;
}
if (clickedChecklist) {
evt.preventDefault();
evt.stopPropagation();
@@ -214,16 +227,7 @@ Template.listBody.onCreated(function () {
return;
}
if (MultiSelection.isActive() || evt.shiftKey) {
evt.stopImmediatePropagation();
evt.preventDefault();
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
MultiSelection[methodName](card._id);
// If the card is already selected, we want to de-select it.
// XXX We should probably modify the minicard href attribute instead of
// overwriting the event in case the card is already selected.
} else if (Utils.isMiniScreen()) {
if (Utils.isMiniScreen()) {
evt.preventDefault();
Session.set('popupCardId', card._id);
this.cardDetailsPopup(evt);
@@ -244,9 +248,18 @@ Template.listBody.onCreated(function () {
};
this.toggleMultiSelection = (evt) => {
evt.stopPropagation();
evt.stopImmediatePropagation();
evt.preventDefault();
MultiSelection.toggle(Template.currentData()._id);
// Resolve the card from the clicked checkbox element. Using
// Blaze.getData(evt.currentTarget) is reliable for the per-card data
// context, whereas Template.currentData() can resolve to the enclosing
// list context and toggle the wrong id (so the checkbox appears to do
// nothing). Falls back to Template.currentData() just in case.
const card =
Blaze.getData(evt.currentTarget) || Template.currentData();
if (card && card._id) {
MultiSelection.toggle(card._id);
}
};
this.cardDetailsPopup = (event) => {
+93 -2
View File
@@ -104,6 +104,26 @@
height: 24px;
float: left;
}
/* On non-board pages (All Boards / home, public, my cards, …) the second bar
holds only a title (e.g. "My Boards"). The fixed 40px bar height plus the
floated h1 with a 1.7em line-height clipped the bottom of the text on iOS
Safari. Vertically center the title in a bar that can grow so it is never
cut off. Scoped to non-board-view so the board header (floated buttons) is
untouched. */
body:not(.board-view) #header #header-main-bar {
display: flex;
align-items: center;
height: auto;
min-height: 40px;
padding-top: 0;
padding-bottom: 0;
}
body:not(.board-view) #header #header-main-bar h1 {
float: none;
line-height: 1.3;
}
#header-quick-access {
color: #fff;
transition: background-color 0.4s;
@@ -157,6 +177,10 @@
#header-quick-access i.fa:hover {
color: #ccc;
}
/* Drag-handle toggle icons 30% smaller than the other quick-access icons. */
#header-quick-access .js-toggle-desktop-drag-handles .fa {
font-size: 0.7em;
}
#header-quick-access #header-user-bar,
#header-quick-access #header-new-board-icon,
#header-quick-access ul li {
@@ -1155,9 +1179,76 @@
display: none;
}
#header-quick-access #header-user-bar {
right: 0px;
right: auto;
padding: 10px;
margin: -8px 0 -10px -10px;
margin: 0 0.5rem;
}
/* ---- Small-screen quick-access bar (ALL browsers, width-based) ----
These rules use width-based selectors (not the .iphone-device class) so
they apply to every browser at small width — Fairphone/Android, narrow
desktop and iPhone alike. Keep every item visible: let the bar wrap and
grow instead of pinning its height and clipping the overflow. */
#header-quick-access {
height: auto !important;
min-height: 28px !important;
overflow: visible !important;
flex-wrap: wrap !important;
align-items: center !important;
row-gap: 4px !important;
}
/* The All Boards page scales this bar up to 2em-3em for every browser, which
is too big. Tame it to a moderate size so ~7 items fit in two wrapped rows.
(.iphone-device variants also cover the board view on detected iPhones.) */
body:not(.board-view) #header-quick-access,
.wrapper ~ #header-quick-access,
.iphone-device #header-quick-access,
.iphone-device .wrapper ~ #header-quick-access,
.iphone-device body:not(.board-view) #header-quick-access {
font-size: 1.2em !important;
}
body:not(.board-view) #header-quick-access .fa,
body:not(.board-view) #header-quick-access .icon,
.wrapper ~ #header-quick-access .fa,
.wrapper ~ #header-quick-access .icon,
.iphone-device #header-quick-access .fa,
.iphone-device #header-quick-access .icon,
.iphone-device .wrapper ~ #header-quick-access .fa,
.iphone-device .wrapper ~ #header-quick-access .icon,
.iphone-device body:not(.board-view) #header-quick-access .fa,
.iphone-device body:not(.board-view) #header-quick-access .icon {
font-size: 1.5em !important;
}
/* Zoom (100%), drag-handle toggle and mobile/desktop toggle 30% smaller than
the other quick-access controls at small width — for all browsers. */
body:not(.board-view) #header-quick-access .js-toggle-desktop-drag-handles .fa,
body:not(.board-view) #header-quick-access .mobile-mode-toggle .fa,
.wrapper ~ #header-quick-access .js-toggle-desktop-drag-handles .fa,
.wrapper ~ #header-quick-access .mobile-mode-toggle .fa,
.iphone-device #header-quick-access .js-toggle-desktop-drag-handles .fa,
.iphone-device #header-quick-access .mobile-mode-toggle .fa,
.iphone-device .wrapper ~ #header-quick-access .js-toggle-desktop-drag-handles .fa,
.iphone-device .wrapper ~ #header-quick-access .mobile-mode-toggle .fa,
.iphone-device body:not(.board-view) #header-quick-access .js-toggle-desktop-drag-handles .fa,
.iphone-device body:not(.board-view) #header-quick-access .mobile-mode-toggle .fa {
font-size: 1.05em !important; /* ~30% smaller than the 1.5em icons */
}
body:not(.board-view) #header-quick-access .zoom-controls,
body:not(.board-view) #header-quick-access .zoom-controls .zoom-level,
body:not(.board-view) #header-quick-access .zoom-controls .zoom-input,
.wrapper ~ #header-quick-access .zoom-controls,
.wrapper ~ #header-quick-access .zoom-controls .zoom-level,
.wrapper ~ #header-quick-access .zoom-controls .zoom-input,
.iphone-device #header-quick-access .zoom-controls,
.iphone-device #header-quick-access .zoom-controls .zoom-level,
.iphone-device #header-quick-access .zoom-controls .zoom-input,
.iphone-device .wrapper ~ #header-quick-access .zoom-controls,
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-level,
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-input,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-level,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-input {
font-size: 0.7em !important; /* ~30% smaller than surrounding text */
}
}
@media print {
+22 -20
View File
@@ -593,12 +593,20 @@ a:not(.disabled).is-active i.fa {
}
#header-quick-access {
/* Keep quick-access items in one row */
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 100%;
/* On narrow screens (e.g. iPhone 12 mini, 375px) the icon buttons cannot
shrink below their min-width, so a single nowrap row overflows and the
base rule's overflow:hidden clips the right-side items (mobile/desktop
toggle, notifications, member/user menu). Allow wrapping to a second row
and let the bar grow so nothing is ever hidden. */
flex-wrap: wrap;
overflow: visible;
height: auto;
min-height: 28px;
}
/* Hide elements that should move to the hamburger menu on mobile */
@@ -607,20 +615,18 @@ a:not(.disabled).is-active i.fa {
display: none !important;
}
/* Show only the home icon (hide the trailing text) on mobile */
/* Show the home link with its full "All Boards" text on mobile. The label is
a bare text node (not a span), so the old max-width:28px + overflow:hidden
clip revealed only "A"; removing it shows the whole label. The bar wraps,
so the extra width is fine. */
#header-quick-access .home-icon a {
display: inline-flex;
align-items: center;
max-width: 28px; /* enough to display the icon */
overflow: hidden;
max-width: none;
overflow: visible;
white-space: nowrap;
}
/* Hide text in home icon on mobile, show only icon */
#header-quick-access .home-icon a span:not(.fa) {
display: none !important;
}
/* Ensure proper spacing for mobile header elements */
#header-quick-access .zoom-controls {
margin-left: auto;
@@ -673,22 +679,18 @@ a:not(.disabled).is-active i.fa {
-webkit-overflow-scrolling: touch;
}
/* Admin panel mobile optimization */
/* Admin panel mobile optimization.
Keep the side-by-side (row) layout from settingBody.css even on narrow
screens — do NOT switch to a stacked column layout, which left the menu
only 32vw wide with empty space beside it and pushed the content off
screen. The side menu keeps a fixed basis and the main body fills the
rest, so both stay visible. */
.setting-content .content-body {
flex-direction: column;
gap: 16px;
gap: 8px;
padding: 8px;
}
.setting-content .content-body .side-menu {
width: 100%;
order: 2;
}
.setting-content .content-body .main-body {
order: 1;
min-height: 60vh;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@@ -0,0 +1,70 @@
/* Search + pagination controls for the admin report tabs (files, rules,
boards, cards). Styled to match the admin People panel (peopleBody.css). */
.admin-report-search {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
height: 34px;
margin-bottom: 12px;
}
.admin-report-search input {
flex: 1;
max-width: 320px;
padding: 0 8px;
height: 34px;
}
.admin-report-search button {
min-width: 90px;
height: 34px;
background: #01628c;
color: #fff;
border: none;
border-radius: 0.4vw;
cursor: pointer;
padding: 0 12px;
}
.admin-report-search button:hover {
background: #005377;
}
.admin-report-search button:active {
background: #004766;
}
.admin-report-pagination {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 12px;
}
.admin-report-pagination button {
min-width: 32px;
height: 30px;
border: 1px solid #bbb;
background: #fff;
border-radius: 0.4vw;
cursor: pointer;
}
.admin-report-pagination button.active {
background: #01628c;
border-color: #01628c;
color: #fff;
}
.admin-report-pagination button.disabled {
opacity: 0.5;
pointer-events: none;
}
.admin-report-page-info {
min-width: 72px;
text-align: center;
color: #333;
}
+53 -9
View File
@@ -8,27 +8,27 @@ template(name="adminReports")
ul
li
a.js-report-broken(data-id="report-broken")
span.emoji-icon 🔗
i.fa.fa-chain-broken
| {{_ 'broken-cards'}}
li
a.js-report-files(data-id="report-files")
span.emoji-icon 📎
i.fa.fa-paperclip
| {{_ 'filesReportTitle'}}
li
a.js-report-rules(data-id="report-rules")
span.emoji-icon ✨
i.fa.fa-magic
| {{_ 'rulesReportTitle'}}
li
a.js-report-boards(data-id="report-boards")
span.emoji-icon ✨
i.fa.fa-columns
| {{_ 'boardsReportTitle'}}
li
a.js-report-cards(data-id="report-cards")
span.emoji-icon ✨
i.fa.fa-id-card-o
| {{_ 'cardsReportTitle'}}
.main-body
@@ -37,13 +37,13 @@ template(name="adminReports")
else if showBrokenCardsReport.get
+brokenCardsReport
else if showFilesReport.get
+filesReport
+filesReport(currentPage=filesCurrentPage totalPages=filesTotalPages hasPrevPage=hasFilesPrevPage hasNextPage=hasFilesNextPage)
else if showRulesReport.get
+rulesReport
+rulesReport(currentPage=rulesCurrentPage totalPages=rulesTotalPages hasPrevPage=hasRulesPrevPage hasNextPage=hasRulesNextPage)
else if showBoardsReport.get
+boardsReport
+boardsReport(currentPage=boardsCurrentPage totalPages=boardsTotalPages hasPrevPage=hasBoardsPrevPage hasNextPage=hasBoardsNextPage)
else if showCardsReport.get
+cardsReport
+cardsReport(currentPage=cardsCurrentPage totalPages=cardsTotalPages hasPrevPage=hasCardsPrevPage hasNextPage=hasCardsNextPage)
template(name="brokenCardsReport")
@@ -56,6 +56,17 @@ template(name="brokenCardsReport")
template(name="rulesReport")
h1 {{_ 'rulesReportTitle'}}
.admin-report-search
input.js-rules-search-input(type="text" placeholder="{{_ 'search'}}")
button.js-rules-search-button
i.fa.fa-search
| {{_ 'search'}}
.admin-report-pagination
button.js-rules-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
| &lt;
span.admin-report-page-info {{currentPage}} / {{totalPages}}
button.js-rules-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
| &gt;
if resultsCount
table
thead
@@ -77,6 +88,17 @@ template(name="rulesReport")
template(name="filesReport")
h1 {{_ 'filesReportTitle'}}
.admin-report-search
input.js-files-search-input(type="text" placeholder="{{_ 'search'}}")
button.js-files-search-button
i.fa.fa-search
| {{_ 'search'}}
.admin-report-pagination
button.js-files-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
| &lt;
span.admin-report-page-info {{currentPage}} / {{totalPages}}
button.js-files-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
| &gt;
if resultsCount
table
thead
@@ -102,6 +124,17 @@ template(name="filesReport")
template(name="cardsReport")
h1 {{_ 'cardsReportTitle'}}
.admin-report-search
input.js-cards-search-input(type="text" placeholder="{{_ 'search'}}")
button.js-cards-search-button
i.fa.fa-search
| {{_ 'search'}}
.admin-report-pagination
button.js-cards-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
| &lt;
span.admin-report-page-info {{currentPage}} / {{totalPages}}
button.js-cards-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
| &gt;
if resultsCount
table.table
thead
@@ -127,6 +160,17 @@ template(name="cardsReport")
template(name="boardsReport")
h1 {{_ 'boardsReportTitle'}}
.admin-report-search
input.js-boards-search-input(type="text" placeholder="{{_ 'search'}}")
button.js-boards-search-button
i.fa.fa-search
| {{_ 'search'}}
.admin-report-pagination
button.js-boards-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
| &lt;
span.admin-report-page-info {{currentPage}} / {{totalPages}}
button.js-boards-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
| &gt;
if resultsCount
table.table
thead
+182 -34
View File
@@ -3,8 +3,6 @@ import { TAPi18n } from '/imports/i18n';
import Attachments, { AttachmentStorage } from '/models/attachments';
import { CardSearchPaged } from '/client/lib/cardSearch';
import SessionData from '/models/usersessiondata';
import { QueryParams } from '/config/query-classes';
import { OPERATOR_LIMIT } from '/config/search-const';
import Boards from '/models/boards';
import Cards from '/models/cards';
import Rules from '/models/rules';
@@ -35,8 +33,12 @@ function abbreviate(text) {
return text;
}
function collectionResults(collection) {
return collection.find();
// The report publications already send only the current page (server-side
// search + limit/skip, sorted). Display exactly what was published, applying
// the same sort so minimongo order matches the server page. Re-applying
// skip/limit here would paginate an already-paginated set.
function collectionResults(collection, sort) {
return collection.find({}, sort ? { sort } : {});
}
function collectionResultsCount(collection) {
@@ -45,6 +47,22 @@ function collectionResultsCount(collection) {
// --- adminReports template ---
// Rows per page for the paginated reports (files, rules, boards, cards).
// Kept in sync with the admin People panel so all admin lists page alike.
const REPORTS_PER_PAGE = 25;
// Static description of each paginated report: which page/count reactive vars
// it uses, which publication feeds it and which count method backs it. Lets a
// single loadReport() drive all of them the same way.
function reportConfig(tmpl) {
return {
'report-files': { page: tmpl.filesPage, count: tmpl.filesCount, search: tmpl.filesSearch, pub: 'attachmentsList', countMethod: 'getAttachmentsReportCount' },
'report-rules': { page: tmpl.rulesPage, count: tmpl.rulesCount, search: tmpl.rulesSearch, pub: 'rulesReport', countMethod: 'getRulesReportCount' },
'report-boards': { page: tmpl.boardsPage, count: tmpl.boardsCount, search: tmpl.boardsSearch, pub: 'boardsReport', countMethod: 'getBoardsReportCount' },
'report-cards': { page: tmpl.cardsPage, count: tmpl.cardsCount, search: tmpl.cardsSearch, pub: 'cardsReport', countMethod: 'getCardsReportCount' },
};
}
Template.adminReports.onCreated(function () {
this.subscription = null;
this.showFilesReport = new ReactiveVar(false);
@@ -56,6 +74,60 @@ Template.adminReports.onCreated(function () {
this.sessionId = SessionData.getSessionId();
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
// Pagination state, one current-page + total-count pair per report.
this.activeReport = new ReactiveVar('');
this.filesPage = new ReactiveVar(1);
this.rulesPage = new ReactiveVar(1);
this.boardsPage = new ReactiveVar(1);
this.cardsPage = new ReactiveVar(1);
this.filesCount = new ReactiveVar(0);
this.rulesCount = new ReactiveVar(0);
this.boardsCount = new ReactiveVar(0);
this.cardsCount = new ReactiveVar(0);
// Current search term per report (empty string = no filter).
this.filesSearch = new ReactiveVar('');
this.rulesSearch = new ReactiveVar('');
this.boardsSearch = new ReactiveVar('');
this.cardsSearch = new ReactiveVar('');
// (Re)subscribe the given report for its current page and refresh its total
// count. Server-side search + limit/skip means only the matching page ever
// reaches minimongo.
this.loadReport = (reportId) => {
const cfg = reportConfig(this)[reportId];
if (!cfg) {
return;
}
// Note: we deliberately do not toggle the global `loading` spinner here.
// The spinner is shown once on tab switch (switchMenu); paging/searching
// re-subscribes silently so the search box keeps focus and the report
// stays mounted, just like the admin People panel.
if (this.subscription) {
this.subscription.stop();
}
const searchTerm = cfg.search.get();
const limit = REPORTS_PER_PAGE;
const skip = (cfg.page.get() - 1) * REPORTS_PER_PAGE;
this.subscription = Meteor.subscribe(cfg.pub, searchTerm, limit, skip, () => {
this.loading.set(false);
});
Meteor.call(cfg.countMethod, searchTerm, (error, count) => {
if (error) {
console.error(`Failed to load ${cfg.countMethod}:`, error);
return;
}
const total = count || 0;
const totalPages = Math.max(1, Math.ceil(total / REPORTS_PER_PAGE));
// If rows were removed while on a now-out-of-range page, clamp and reload.
if (cfg.page.get() > totalPages) {
cfg.page.set(totalPages);
this.loadReport(reportId);
return;
}
cfg.count.set(total);
});
};
});
Template.adminReports.helpers({
@@ -80,6 +152,39 @@ Template.adminReports.helpers({
loading() {
return Template.instance().loading;
},
// --- Pagination helpers, passed down into each report sub-template ---
filesCurrentPage() { return Template.instance().filesPage.get(); },
filesTotalPages() { return Math.max(1, Math.ceil((Template.instance().filesCount.get() || 0) / REPORTS_PER_PAGE)); },
hasFilesPrevPage() { return Template.instance().filesPage.get() > 1; },
hasFilesNextPage() {
const tpl = Template.instance();
return tpl.filesPage.get() < Math.max(1, Math.ceil((tpl.filesCount.get() || 0) / REPORTS_PER_PAGE));
},
rulesCurrentPage() { return Template.instance().rulesPage.get(); },
rulesTotalPages() { return Math.max(1, Math.ceil((Template.instance().rulesCount.get() || 0) / REPORTS_PER_PAGE)); },
hasRulesPrevPage() { return Template.instance().rulesPage.get() > 1; },
hasRulesNextPage() {
const tpl = Template.instance();
return tpl.rulesPage.get() < Math.max(1, Math.ceil((tpl.rulesCount.get() || 0) / REPORTS_PER_PAGE));
},
boardsCurrentPage() { return Template.instance().boardsPage.get(); },
boardsTotalPages() { return Math.max(1, Math.ceil((Template.instance().boardsCount.get() || 0) / REPORTS_PER_PAGE)); },
hasBoardsPrevPage() { return Template.instance().boardsPage.get() > 1; },
hasBoardsNextPage() {
const tpl = Template.instance();
return tpl.boardsPage.get() < Math.max(1, Math.ceil((tpl.boardsCount.get() || 0) / REPORTS_PER_PAGE));
},
cardsCurrentPage() { return Template.instance().cardsPage.get(); },
cardsTotalPages() { return Math.max(1, Math.ceil((Template.instance().cardsCount.get() || 0) / REPORTS_PER_PAGE)); },
hasCardsPrevPage() { return Template.instance().cardsPage.get() > 1; },
hasCardsNextPage() {
const tpl = Template.instance();
return tpl.cardsPage.get() < Math.max(1, Math.ceil((tpl.cardsCount.get() || 0) / REPORTS_PER_PAGE));
},
});
Template.adminReports.events({
@@ -98,8 +203,60 @@ Template.adminReports.events({
'click a.js-report-boards'(event) {
switchMenu(event, Template.instance());
},
// --- Pagination controls (buttons live inside the report sub-templates,
// their clicks bubble up to this parent template) ---
'click .js-files-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-files'); },
'click .js-files-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-files'); },
'click .js-rules-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-rules'); },
'click .js-rules-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-rules'); },
'click .js-boards-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-boards'); },
'click .js-boards-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-boards'); },
'click .js-cards-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-cards'); },
'click .js-cards-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-cards'); },
// --- Search (one input + button per report, mirrors the People panel) ---
'click .js-files-search-button'(event, tmpl) { runSearch(tmpl, 'report-files', '.js-files-search-input'); },
'keydown .js-files-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-files', '.js-files-search-input'); },
'click .js-rules-search-button'(event, tmpl) { runSearch(tmpl, 'report-rules', '.js-rules-search-input'); },
'keydown .js-rules-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-rules', '.js-rules-search-input'); },
'click .js-boards-search-button'(event, tmpl) { runSearch(tmpl, 'report-boards', '.js-boards-search-input'); },
'keydown .js-boards-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-boards', '.js-boards-search-input'); },
'click .js-cards-search-button'(event, tmpl) { runSearch(tmpl, 'report-cards', '.js-cards-search-input'); },
'keydown .js-cards-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-cards', '.js-cards-search-input'); },
});
function goPrevPage(event, tmpl, reportId) {
event.preventDefault();
const cfg = reportConfig(tmpl)[reportId];
const current = cfg.page.get();
if (current > 1) {
cfg.page.set(current - 1);
tmpl.loadReport(reportId);
}
}
function goNextPage(event, tmpl, reportId) {
event.preventDefault();
const cfg = reportConfig(tmpl)[reportId];
const total = cfg.count.get() || 0;
const totalPages = Math.max(1, Math.ceil(total / REPORTS_PER_PAGE));
const current = cfg.page.get();
if (current < totalPages) {
cfg.page.set(current + 1);
tmpl.loadReport(reportId);
}
}
// Read the report's search box, store the term, reset to page 1 and reload.
function runSearch(tmpl, reportId, inputSelector) {
const cfg = reportConfig(tmpl)[reportId];
const value = (tmpl.$(inputSelector).val() || '').trim();
cfg.search.set(value);
cfg.page.set(1);
tmpl.loadReport(reportId);
}
function switchMenu(event, tmpl) {
const target = $(event.target);
if (!target.hasClass('active')) {
@@ -117,6 +274,7 @@ function switchMenu(event, tmpl) {
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
const targetID = target.data('id');
tmpl.activeReport.set(targetID);
if ('report-broken' === targetID) {
tmpl.showBrokenCardsReport.set(true);
@@ -129,32 +287,24 @@ function switchMenu(event, tmpl) {
);
} else if ('report-files' === targetID) {
tmpl.showFilesReport.set(true);
tmpl.subscription = Meteor.subscribe('attachmentsList', () => {
tmpl.loading.set(false);
});
tmpl.filesPage.set(1);
tmpl.filesSearch.set('');
tmpl.loadReport('report-files');
} else if ('report-rules' === targetID) {
tmpl.subscription = Meteor.subscribe('rulesReport', () => {
tmpl.showRulesReport.set(true);
tmpl.loading.set(false);
});
tmpl.showRulesReport.set(true);
tmpl.rulesPage.set(1);
tmpl.rulesSearch.set('');
tmpl.loadReport('report-rules');
} else if ('report-boards' === targetID) {
tmpl.subscription = Meteor.subscribe('boardsReport', () => {
tmpl.showBoardsReport.set(true);
tmpl.loading.set(false);
});
tmpl.showBoardsReport.set(true);
tmpl.boardsPage.set(1);
tmpl.boardsSearch.set('');
tmpl.loadReport('report-boards');
} else if ('report-cards' === targetID) {
const qp = new QueryParams();
qp.addPredicate(OPERATOR_LIMIT, 300);
tmpl.subscription = Meteor.subscribe(
'globalSearch',
tmpl.sessionId,
qp.getParams(),
qp.text,
() => {
tmpl.showCardsReport.set(true);
tmpl.loading.set(false);
},
);
tmpl.showCardsReport.set(true);
tmpl.cardsPage.set(1);
tmpl.cardsSearch.set('');
tmpl.loadReport('report-cards');
}
}
}
@@ -163,7 +313,7 @@ function switchMenu(event, tmpl) {
Template.filesReport.helpers({
results() {
return collectionResults(Attachments);
return collectionResults(Attachments, { name: 1 });
},
resultsCount() {
return collectionResultsCount(Attachments);
@@ -179,19 +329,17 @@ Template.rulesReport.helpers({
results() {
const rules = [];
ReactiveCache.getRules().forEach(rule => {
ReactiveCache.getRules({}, { sort: { boardId: 1 } }).forEach(rule => {
rules.push({
_id: rule._id,
title: rule.title,
boardId: rule.boardId,
boardTitle: rule.board().title,
boardTitle: rule.board()?.title,
action: rule.action(),
trigger: rule.trigger(),
});
});
// eslint-disable-next-line no-console
console.log('rows:', rules);
return rules;
},
resultsCount() {
@@ -203,7 +351,7 @@ Template.rulesReport.helpers({
Template.boardsReport.helpers({
results() {
return collectionResults(Boards);
return collectionResults(Boards, { sort: 1 });
},
resultsCount() {
return collectionResultsCount(Boards);
@@ -247,7 +395,7 @@ Template.boardsReport.helpers({
Template.cardsReport.helpers({
results() {
return collectionResults(Cards);
return collectionResults(Cards, { boardId: 1, sort: 1 });
},
resultsCount() {
return collectionResultsCount(Cards);
@@ -204,3 +204,39 @@
white-space: nowrap;
min-width: 0 !important;
}
/* Cloud storage (S3 / Azure / GCS): example value shown below each input. */
.attachments-content .cloud-input-example {
display: block;
color: #888;
font-size: 0.82em;
font-family: monospace;
word-break: break-all;
line-height: 1.35;
}
/* Cloud storage: where-to-find-this-value path shown below the example. */
.attachments-content .cloud-input-path {
display: block;
color: #6a737d;
font-size: 0.82em;
line-height: 1.4;
margin-top: 2px;
}
/* Cloud storage: translated label shown under the (English) label. */
.attachments-content .cloud-input-label-tr {
display: block;
color: #57606a;
font-size: 0.9em;
margin-bottom: 4px;
}
/* Cloud storage: description text (English literal and translated). */
.attachments-content .cloud-input-desc {
display: block;
color: #6a737d;
font-size: 0.82em;
line-height: 1.4;
margin-top: 2px;
}
+280 -129
View File
@@ -16,6 +16,11 @@ template(name="attachments")
span.emoji-icon
i.fa.fa-arrow-right
| {{_ 'attachment-move'}}
li(class="{{#if isDefaultStorageActive}}active{{/if}}")
a.js-attachments-menu(data-id="default-save-storage")
span.emoji-icon
i.fa.fa-save
| {{_ 'default-save-storage'}}
li(class="{{#if isLimitsActive}}active{{/if}}")
a.js-attachments-menu(data-id="limits")
span.emoji-icon
@@ -36,6 +41,16 @@ template(name="attachments")
span.emoji-icon
i.fa.fa-cloud
| {{_ 's3-minio-storage'}}
li(class="{{#if isAzureActive}}active{{/if}}")
a.js-attachments-menu(data-id="azure")
span.emoji-icon
i.fa.fa-cloud
| {{_ 'azure-blob-storage'}}
li(class="{{#if isGcsActive}}active{{/if}}")
a.js-attachments-menu(data-id="gcs")
span.emoji-icon
i.fa.fa-cloud
| {{_ 'gcs-storage'}}
.main-body
if loading.get
@@ -45,6 +60,17 @@ template(name="attachments")
li
h3 {{_ 'attachment-move'}}
+moveAttachments
else if isDefaultStorageActive
ul#attachment-default-storage-setting.setting-detail
li
h3 {{_ 'default-save-storage'}}
.description {{_ 'default-save-storage-description'}}
.form-group
select.wekan-form-control.js-default-save-storage
each defaultStorageOptions
option(value=value selected=selected) {{_ labelKey}}
.form-group
button.js-save-default-storage.primary {{_ 'save'}}
else if isLimitsActive
ul#attachment-limits-setting.setting-detail
li
@@ -109,9 +135,6 @@ template(name="attachments")
.storage-rw-item
input.js-toggle-filesystem-read#filesystem-read(type="checkbox" checked="{{filesystemRead}}")
label(for="filesystem-read") {{_ 'storage-read'}}
.storage-rw-item
input.js-toggle-filesystem-write#filesystem-write(type="checkbox" checked="{{filesystemWrite}}")
label(for="filesystem-write") {{_ 'save'}}
.form-group
button.js-calculate-filesystem-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
@@ -150,13 +173,11 @@ template(name="attachments")
ul#attachment-gridfs-setting.setting-detail
li
h3 {{_ 'mongodb-gridfs-storage'}}
.description {{_ 'gridfs-move-collectionfs-note'}}
.storage-rw-row
.storage-rw-item
input.js-toggle-gridfs-read#gridfs-read(type="checkbox" checked="{{gridfsRead}}")
label(for="gridfs-read") {{_ 'storage-read'}}
.storage-rw-item
input.js-toggle-gridfs-write#gridfs-write(type="checkbox" checked="{{gridfsWrite}}")
label(for="gridfs-write") {{_ 'save'}}
.form-group
button.js-calculate-gridfs-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if gridFsStatsLoading
@@ -199,13 +220,17 @@ template(name="attachments")
ul#attachment-s3-setting.setting-detail
li
h3 {{_ 's3-minio-storage'}}
.description {{_ 's3-minio-storage-description'}}
.storage-rw-row
.storage-rw-item
input#s3-read(type="checkbox" checked="{{s3Read}}" disabled)
label(for="s3-read") {{_ 'storage-read'}}
input#s3-enabled(type="checkbox" checked=cloudEnabled.s3)
label(for="s3-enabled") {{_ 'storage-enabled'}}
.storage-rw-item
input#s3-write(type="checkbox" checked="{{s3Write}}" disabled)
label(for="s3-write") {{_ 'save'}}
input#s3-read(type="checkbox" checked=cloudRead.s3)
label(for="s3-read") {{_ 'storage-read'}}
.form-group
button.js-save-cloud-settings.btn.btn-primary(data-provider="s3") {{_ 'save'}}
.form-group
button.js-calculate-s3-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
@@ -226,84 +251,289 @@ template(name="attachments")
td {{s3Stats.avatars}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
label Endpoint
small.form-text.cloud-input-label-tr {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value=cloudValue.s3.endpoint)
small.form-text.cloud-input-example https://s3.eu-west-1.amazonaws.com
small.form-text.cloud-input-desc S3 endpoint URL (e.g., s3.amazonaws.com or minio.example.com)
small.form-text.cloud-input-desc {{_ 's3-endpoint-description'}}
small.form-text.cloud-input-path AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.
small.form-text.cloud-input-path {{_ 's3-endpoint-menu-path'}}
.form-group
label AWS Region
small.form-text.cloud-input-label-tr {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value=cloudValue.s3.region)
small.form-text.cloud-input-example eu-west-1
small.form-text.cloud-input-desc AWS S3 region (e.g., us-east-1)
small.form-text.cloud-input-desc {{_ 's3-region-description'}}
small.form-text.cloud-input-path AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.
small.form-text.cloud-input-path {{_ 's3-region-menu-path'}}
.form-group
label Bucket name
small.form-text.cloud-input-label-tr {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value=cloudValue.s3.bucket)
small.form-text.cloud-input-example wekan-s3-test-369341155299-eu-west-1-an
small.form-text.cloud-input-desc S3 bucket name for storing files
small.form-text.cloud-input-desc {{_ 's3-bucket-description'}}
small.form-text.cloud-input-path AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).
small.form-text.cloud-input-path {{_ 's3-bucket-menu-path'}}
.form-group
label Access key ID
small.form-text.cloud-input-label-tr {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" value=cloudValue.s3.accessKeyId)
small.form-text.cloud-input-example AKIAIOSFODNN7EXAMPLE
small.form-text.cloud-input-desc AWS S3 access key for authentication
small.form-text.cloud-input-desc {{_ 's3-access-key-description'}}
small.form-text.cloud-input-path AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.
small.form-text.cloud-input-path {{_ 's3-access-key-menu-path'}}
.form-group
label Secret access key
small.form-text.cloud-input-label-tr {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder=cloudSecretPlaceholder.s3.secretAccessKey)
small.form-text.cloud-input-example wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
small.form-text.cloud-input-desc Leave blank to keep the current value.
small.form-text.cloud-input-desc {{_ 'cloud-secret-keep-blank'}}
small.form-text.cloud-input-path Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).
small.form-text.cloud-input-path {{_ 's3-secret-key-menu-path'}}
.form-group
.storage-rw-item
input#s3-force-path-style(type="checkbox" checked=cloudValue.s3.forcePathStyle)
label(for="s3-force-path-style") {{_ 's3-force-path-style'}}
small.form-text.text-muted {{_ 's3-force-path-style-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
button.js-test-cloud-connection.btn.btn-secondary(data-provider="s3") {{_ 'test-cloud-connection'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="s3") {{_ 'save'}}
if cloudTestResult.s3
p.form-text.text-success {{_ 'cloud-connection-success'}}
if cloudTestError.s3
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.s3}}
else if isAzureActive
ul#attachment-azure-setting.setting-detail
li
h3 {{_ 'azure-blob-storage'}}
.storage-rw-row
.storage-rw-item
input#azure-enabled(type="checkbox" checked=cloudEnabled.azure)
label(for="azure-enabled") {{_ 'storage-enabled'}}
.storage-rw-item
input#azure-read(type="checkbox" checked=cloudRead.azure)
label(for="azure-read") {{_ 'storage-read'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="azure") {{_ 'save'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
button.js-calculate-azure-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if azureStatsLoading
p.form-text.text-muted {{_ 'calculating-counts'}}
if azureStatsError
p.form-text.text-danger {{azureStatsError}}
if hasAzureStats
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'stats-count'}}
tr
td {{_ 'attachments'}}
td {{azureStats.attachments}}
tr
td {{_ 'avatars'}}
td {{azureStats.avatars}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
label Storage account name
small.form-text.cloud-input-label-tr {{_ 'azure-account-name'}}
input.wekan-form-control#azure-account-name(type="text" value=cloudValue.azure.accountName)
small.form-text.cloud-input-example wekanstorage
small.form-text.cloud-input-desc The name of your Azure Storage account.
small.form-text.cloud-input-desc {{_ 'azure-account-name-description'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).
small.form-text.cloud-input-path {{_ 'azure-account-name-menu-path'}}
.form-group
label Storage account key
small.form-text.cloud-input-label-tr {{_ 'azure-account-key'}}
input.wekan-form-control#azure-account-key(type="password" placeholder=cloudSecretPlaceholder.azure.accountKey)
small.form-text.cloud-input-example base64AccountKey==
small.form-text.cloud-input-desc Leave blank to keep the current value.
small.form-text.cloud-input-desc {{_ 'cloud-secret-keep-blank'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.
small.form-text.cloud-input-path {{_ 'azure-account-key-menu-path'}}
.form-group
label Connection string
small.form-text.cloud-input-label-tr {{_ 'azure-connection-string'}}
input.wekan-form-control#azure-connection-string(type="password" placeholder=cloudSecretPlaceholder.azure.connectionString)
small.form-text.cloud-input-example DefaultEndpointsProtocol=https;AccountName=wekanstorage;AccountKey=...;EndpointSuffix=core.windows.net
small.form-text.cloud-input-desc Optional: use a full connection string instead of account name and key.
small.form-text.cloud-input-desc {{_ 'azure-connection-string-description'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.
small.form-text.cloud-input-path {{_ 'azure-connection-string-menu-path'}}
.form-group
label Container name
small.form-text.cloud-input-label-tr {{_ 'azure-container'}}
input.wekan-form-control#azure-bucket(type="text" value=cloudValue.azure.bucket)
small.form-text.cloud-input-example wekan-attachments
small.form-text.cloud-input-desc The blob container to store files in.
small.form-text.cloud-input-desc {{_ 'azure-container-description'}}
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).
small.form-text.cloud-input-path {{_ 'azure-container-menu-path'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
button.js-test-cloud-connection.btn.btn-secondary(data-provider="azure") {{_ 'test-cloud-connection'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="azure") {{_ 'save'}}
if cloudTestResult.azure
p.form-text.text-success {{_ 'cloud-connection-success'}}
if cloudTestError.azure
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.azure}}
else if isGcsActive
ul#attachment-gcs-setting.setting-detail
li
h3 {{_ 'gcs-storage'}}
.description Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In "New principals", paste the service account email — the "client_email" value from your JSON key — then choose the role "Storage Object Admin" and Save.
.description {{_ 'gcs-permissions-note'}}
.storage-rw-row
.storage-rw-item
input#gcs-enabled(type="checkbox" checked=cloudEnabled.gcs)
label(for="gcs-enabled") {{_ 'storage-enabled'}}
.storage-rw-item
input#gcs-read(type="checkbox" checked=cloudRead.gcs)
label(for="gcs-read") {{_ 'storage-read'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="gcs") {{_ 'save'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
button.js-calculate-gcs-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
if gcsStatsLoading
p.form-text.text-muted {{_ 'calculating-counts'}}
if gcsStatsError
p.form-text.text-danger {{gcsStatsError}}
if hasGcsStats
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'stats-count'}}
tr
td {{_ 'attachments'}}
td {{gcsStats.attachments}}
tr
td {{_ 'avatars'}}
td {{gcsStats.avatars}}
.form-group
label Project ID
small.form-text.cloud-input-label-tr {{_ 'gcs-project-id'}}
input.wekan-form-control#gcs-project-id(type="text" value=cloudValue.gcs.projectId)
small.form-text.cloud-input-example my-gcp-project-123456
small.form-text.cloud-input-desc Your Google Cloud project id.
small.form-text.cloud-input-desc {{_ 'gcs-project-id-description'}}
small.form-text.cloud-input-path Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).
small.form-text.cloud-input-path {{_ 'gcs-project-id-menu-path'}}
.form-group
label Bucket name
small.form-text.cloud-input-label-tr {{_ 'gcs-bucket'}}
input.wekan-form-control#gcs-bucket(type="text" value=cloudValue.gcs.bucket)
small.form-text.cloud-input-example wekan-attachments
small.form-text.cloud-input-desc The name of the Cloud Storage bucket to store files in.
small.form-text.cloud-input-desc {{_ 'gcs-bucket-description'}}
small.form-text.cloud-input-path Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).
small.form-text.cloud-input-path {{_ 'gcs-bucket-menu-path'}}
.form-group
label Service account key file path
small.form-text.cloud-input-label-tr {{_ 'gcs-key-filename'}}
input.wekan-form-control#gcs-key-filename(type="text" value=cloudValue.gcs.keyFilename)
small.form-text.cloud-input-example /data/gcs-service-account.json
small.form-text.cloud-input-desc Optional: path to a service account JSON key file on the server.
small.form-text.cloud-input-desc {{_ 'gcs-key-filename-description'}}
small.form-text.cloud-input-path Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.
small.form-text.cloud-input-path {{_ 'gcs-key-filename-menu-path'}}
.form-group
label Service account key (JSON)
small.form-text.cloud-input-label-tr {{_ 'gcs-credentials'}}
textarea.wekan-form-control#gcs-credentials(rows="4" placeholder=cloudSecretPlaceholder.gcs.credentials)
small.form-text.cloud-input-example { "type": "service_account", "project_id": "my-gcp-project", "client_email": "wekan@my-gcp-project.iam.gserviceaccount.com", "private_key": "-----BEGIN PRIVATE KEY-----..." }
small.form-text.cloud-input-desc Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.
small.form-text.cloud-input-desc {{_ 'gcs-credentials-description'}}
small.form-text.cloud-input-path Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.
small.form-text.cloud-input-path {{_ 'gcs-credentials-menu-path'}}
small.form-text.text-muted {{_ 'gcs-credentials-description'}}
.form-group
button.js-test-cloud-connection.btn.btn-secondary(data-provider="gcs") {{_ 'test-cloud-connection'}}
button.js-save-cloud-settings.btn.btn-primary(data-provider="gcs") {{_ 'save'}}
if cloudTestResult.gcs
p.form-text.text-success {{_ 'cloud-connection-success'}}
if cloudTestError.gcs
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.gcs}}
template(name="moveAttachments")
.move-storage-form
.move-storage-selects
.form-group
label {{_ 'move-scope'}}
select.wekan-form-control.js-move-scope
option(value="attachments") {{_ 'move-scope-attachments'}}
option(value="avatars") {{_ 'move-scope-avatars'}}
option(value="both") {{_ 'move-scope-both'}}
.form-group
label {{_ 'move-source'}}
select.wekan-form-control.js-move-source-storage
option(value="all") {{_ 'move-storage-all'}}
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
option(value="gridfs") {{_ 'move-storage-gridfs'}}
option(value="fs") {{_ 'move-storage-fs'}}
option(value="s3") {{_ 'move-storage-s3'}}
option(value="azure") {{_ 'move-storage-azure'}}
option(value="gcs") {{_ 'move-storage-gcs'}}
.form-group
label {{_ 'move-destination'}}
select.wekan-form-control.js-move-dest-storage
option(value="gridfs") {{_ 'move-storage-gridfs'}}
option(value="fs") {{_ 'move-storage-fs'}}
option(value="s3") {{_ 'move-storage-s3'}}
option(value="azure") {{_ 'move-storage-azure'}}
option(value="gcs") {{_ 'move-storage-gcs'}}
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
button.js-move-all-attachments.btn.btn-primary {{_ 'move-all-attachments'}}
.move-compact-section
h4 {{_ 'mongodb-compact'}}
p.form-text {{_ 'mongodb-compact-description'}}
p.form-text.text-muted {{_ 'mongodb-compact-warning'}}
button.js-compact-mongodb-move.btn.btn-warning(type="button") {{_ 'mongodb-compact-run'}}
if moveCompactLoading
p.form-text.text-muted {{_ 'mongodb-compact-running'}}
if moveCompactError
p.form-text.text-danger {{_ 'mongodb-compact-error'}} {{moveCompactError}}
if moveCompactResult
p.form-text.text-success {{_ 'mongodb-compact-success'}}
ul.compact-result-list
each item in moveCompactResultItems
li
strong {{item.name}}
| : {{item.status}}
if lastMove
.move-last-operation
span.emoji-icon
i.fa.fa-info-circle
| {{_ 'attachment-last-move'}}: {{lastMoveText}}
.move-repair-section
h4 {{_ 'attachment-repair-locations'}}
.description {{_ 'attachment-repair-locations-description'}}
button.js-repair-attachment-locations.btn.btn-secondary(type="button") {{_ 'attachment-repair-locations'}}
if repairLoading
p.form-text.text-muted {{_ 'attachment-repair-running'}}
if repairError
p.form-text.text-danger {{repairError}}
if repairResult
p.form-text.text-success {{_ 'attachment-repair-done'}}
table.gridfs-stats-table
tr
th {{_ 'stats-scope'}}
th {{_ 'attachment-repair-scanned'}}
th {{_ 'attachment-repair-repaired'}}
th {{_ 'attachment-repair-broken'}}
tr
td {{_ 'attachments'}}
td {{repairResult.attachments.scanned}}
td {{repairResult.attachments.repaired}}
td {{repairResult.attachments.broken}}
tr
td {{_ 'avatars'}}
td {{repairResult.avatars.scanned}}
td {{repairResult.avatars.repaired}}
td {{repairResult.avatars.broken}}
if moveProgress
.move-progress-section
.move-progress-header
+spinner
.move-progress-info
.move-progress-counter {{_ 'move-progress-file'}} {{moveProgress.current}} / {{moveProgress.total}}
.move-progress-filename {{moveProgress.name}} ({{moveProgressSize}})
@@ -317,82 +547,3 @@ template(name="moveAttachments")
else
button.js-pause-move.btn.btn-secondary {{_ 'move-progress-pause'}}
button.js-cancel-move.btn.btn-danger {{_ 'move-progress-cancel'}}
each board in getBoardsWithAttachments
+moveBoardAttachments board
template(name="moveBoardAttachments")
hr
.board-description
table
tr
th {{_ 'board-id'}}
th {{_ 'board-title'}}
tr
td {{ _id }}
td {{ title }}
.move-storage-form
.move-storage-selects
.form-group
label {{_ 'move-source'}}
select.wekan-form-control.js-move-board-source-storage
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
option(value="gridfs") {{_ 'move-storage-gridfs'}}
option(value="fs") {{_ 'move-storage-fs'}}
option(value="s3") {{_ 'move-storage-s3'}}
.form-group
label {{_ 'move-destination'}}
select.wekan-form-control.js-move-board-dest-storage
option(value="gridfs") {{_ 'move-storage-gridfs'}}
option(value="fs") {{_ 'move-storage-fs'}}
option(value="s3") {{_ 'move-storage-s3'}}
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
button.js-move-all-attachments-of-board.btn.btn-primary {{_ 'move-all-attachments'}}
.board-attachments
table
tr
th {{_ 'card-id'}}
th {{_ 'attachment-id'}}
th {{_ 'name'}}
th {{_ 'path'}}
th {{_ 'version-name'}}
th {{_ 'size'}}
th {{_ 'gridfs-file-id'}}
th {{_ 's3-file-id'}}
th {{_ 'storage'}}
th {{_ 'action'}}
each attachment in attachments
+moveAttachment attachment
template(name="moveAttachment")
each version in flatVersion
tr
td {{ meta.cardId }}
td {{ _id }}
td {{ name }}
td {{ version.path }}
td {{ version.versionName }}
td {{ fileSize version.size }}
td {{ version.meta.gridFsFileId }}
td {{ version.meta.s3FileId }}
td {{ version.storageName }}
td
if $neq version.storageName "fs"
button.js-move-storage-fs
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-fs'}}
if $neq version.storageName "gridfs"
if version.storageName
button.js-move-storage-gridfs
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-gridfs'}}
if $neq version.storageName "s3"
if version.storageName
button.js-move-storage-s3
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-s3'}}
+348 -198
View File
@@ -1,9 +1,61 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveCache } from '/imports/reactiveCache';
import { groupBy } from '/imports/lib/collectionHelpers';
import Attachments from '/models/attachments';
import AttachmentStorageSettings from '/models/attachmentStorageSettings';
import AttachmentBulkMoveStatus from '/models/attachmentBulkMoveStatus';
import { TAPi18n } from '/imports/i18n';
// DOM field ids per cloud provider, used to read the admin form on save/test.
const CLOUD_FIELD_IDS = {
s3: {
enabled: '#s3-enabled',
read: '#s3-read',
endpoint: '#s3-endpoint',
region: '#s3-region',
bucket: '#s3-bucket',
accessKeyId: '#s3-access-key',
secretAccessKey: '#s3-secret-key',
forcePathStyle: '#s3-force-path-style',
},
azure: {
enabled: '#azure-enabled',
read: '#azure-read',
accountName: '#azure-account-name',
accountKey: '#azure-account-key',
connectionString: '#azure-connection-string',
bucket: '#azure-bucket',
},
gcs: {
enabled: '#gcs-enabled',
read: '#gcs-read',
projectId: '#gcs-project-id',
bucket: '#gcs-bucket',
keyFilename: '#gcs-key-filename',
credentials: '#gcs-credentials',
},
};
const CLOUD_PROVIDERS = ['s3', 'azure', 'gcs'];
function gatherCloudConfig(tpl, provider) {
const ids = CLOUD_FIELD_IDS[provider] || {};
const cfg = {};
Object.keys(ids).forEach(field => {
const el = tpl.$(ids[field]);
if (!el || !el.length) return;
if (el.attr('type') === 'checkbox') {
cfg[field] = el.prop('checked');
} else {
// Trim so stray whitespace/newlines from copy-paste (e.g. an account name
// or endpoint) do not produce an invalid URL in the storage adapter.
const value = el.val();
cfg[field] = typeof value === 'string' ? value.trim() : value;
}
});
return cfg;
}
function cloudConfigFromSettings(tpl, provider) {
const settings = tpl.attachmentStorageSettings.get();
return (settings && settings.storageConfig && settings.storageConfig[provider]) || {};
}
const { filesize } = require('filesize');
const LIMIT_UNIT_FACTORS = {
@@ -178,7 +230,6 @@ function refreshAttachmentStorageSettings(tpl) {
Template.attachments.onCreated(function () {
this.activeSection = new ReactiveVar('move');
this.attachmentsSubscription = Meteor.subscribe('attachmentsList');
this.storageSettingsSubscription = Meteor.subscribe('attachmentStorageSettings');
this.attachmentStorageSettings = new ReactiveVar(null);
this.attachmentLimitUnits = new ReactiveVar({
@@ -202,13 +253,21 @@ Template.attachments.onCreated(function () {
this.s3Stats = new ReactiveVar(null);
this.s3StatsLoading = new ReactiveVar(false);
this.s3StatsError = new ReactiveVar('');
this.azureStats = new ReactiveVar(null);
this.azureStatsLoading = new ReactiveVar(false);
this.azureStatsError = new ReactiveVar('');
this.gcsStats = new ReactiveVar(null);
this.gcsStatsLoading = new ReactiveVar(false);
this.gcsStatsError = new ReactiveVar('');
this.compactLoading = new ReactiveVar(false);
this.compactResult = new ReactiveVar(null);
this.compactError = new ReactiveVar('');
this.cloudTestResults = new ReactiveVar({});
this.cloudTestErrors = new ReactiveVar({});
this.loading = new ReactiveVar(false);
this.autorun(() => {
const ready = this.attachmentsSubscription.ready() && this.storageSettingsSubscription.ready();
const ready = this.storageSettingsSubscription.ready();
if (ready) {
refreshAttachmentStorageSettings(this);
this.loading.set(false);
@@ -228,6 +287,88 @@ Template.attachments.helpers({
isMoveActive() {
return Template.instance().activeSection.get() === 'move';
},
isDefaultStorageActive() {
return Template.instance().activeSection.get() === 'default-save-storage';
},
isAzureActive() {
return Template.instance().activeSection.get() === 'azure';
},
isGcsActive() {
return Template.instance().activeSection.get() === 'gcs';
},
defaultStorageOptions() {
const settings = Template.instance().attachmentStorageSettings.get();
const selected = settings?.defaultStorage || 'fs';
const sc = settings?.storageConfig || {};
const options = [
{ value: 'fs', labelKey: 'move-storage-fs' },
{ value: 'gridfs', labelKey: 'move-storage-gridfs' },
];
// Only offer a cloud backend as a save target once it is enabled.
if (sc.s3?.enabled) options.push({ value: 's3', labelKey: 'move-storage-s3' });
if (sc.azure?.enabled) options.push({ value: 'azure', labelKey: 'move-storage-azure' });
if (sc.gcs?.enabled) options.push({ value: 'gcs', labelKey: 'move-storage-gcs' });
return options.map(option => ({
...option,
selected: option.value === selected,
}));
},
cloudEnabled() {
const tpl = Template.instance();
const out = {};
CLOUD_PROVIDERS.forEach(p => { out[p] = cloudConfigFromSettings(tpl, p).enabled === true; });
return out;
},
cloudRead() {
const tpl = Template.instance();
const out = {};
CLOUD_PROVIDERS.forEach(p => { out[p] = cloudConfigFromSettings(tpl, p).read !== false; });
return out;
},
cloudValue() {
const tpl = Template.instance();
const s3 = cloudConfigFromSettings(tpl, 's3');
const azure = cloudConfigFromSettings(tpl, 'azure');
const gcs = cloudConfigFromSettings(tpl, 'gcs');
return {
s3: {
endpoint: s3.endpoint || '',
region: s3.region || '',
bucket: s3.bucket || '',
accessKeyId: s3.accessKeyId || '',
forcePathStyle: s3.forcePathStyle !== false,
},
azure: {
accountName: azure.accountName || '',
bucket: azure.bucket || '',
},
gcs: {
projectId: gcs.projectId || '',
bucket: gcs.bucket || '',
keyFilename: gcs.keyFilename || '',
},
};
},
cloudSecretPlaceholder() {
const tpl = Template.instance();
const ph = (cfg, field) => (cfg[`${field}Set`]
? TAPi18n.__('cloud-secret-set')
: TAPi18n.__('cloud-secret-none'));
const s3 = cloudConfigFromSettings(tpl, 's3');
const azure = cloudConfigFromSettings(tpl, 'azure');
const gcs = cloudConfigFromSettings(tpl, 'gcs');
return {
s3: { secretAccessKey: ph(s3, 'secretAccessKey') },
azure: { accountKey: ph(azure, 'accountKey'), connectionString: ph(azure, 'connectionString') },
gcs: { credentials: ph(gcs, 'credentials') },
};
},
cloudTestResult() {
return Template.instance().cloudTestResults.get();
},
cloudTestError() {
return Template.instance().cloudTestErrors.get();
},
isGridFsActive() {
return Template.instance().activeSection.get() === 'gridfs';
},
@@ -260,10 +401,6 @@ Template.attachments.helpers({
const settings = Template.instance().attachmentStorageSettings.get();
return settings?.storageConfig?.filesystem?.read !== false;
},
filesystemWrite() {
const settings = Template.instance().attachmentStorageSettings.get();
return settings?.storageConfig?.filesystem?.write !== false;
},
gridfsEnabled() {
const tpl = Template.instance();
const settings = tpl.attachmentStorageSettings.get();
@@ -276,16 +413,9 @@ Template.attachments.helpers({
const settings = Template.instance().attachmentStorageSettings.get();
return settings?.storageConfig?.gridfs?.read !== false;
},
gridfsWrite() {
const settings = Template.instance().attachmentStorageSettings.get();
return settings?.storageConfig?.gridfs?.write !== false;
},
s3Read() {
return process.env.S3_ENABLED === 'true';
},
s3Write() {
return process.env.S3_ENABLED === 'true';
},
compactLoading() {
return Template.instance().compactLoading.get();
},
@@ -346,6 +476,30 @@ Template.attachments.helpers({
s3StatsError() {
return Template.instance().s3StatsError.get();
},
hasAzureStats() {
return !!Template.instance().azureStats.get();
},
azureStats() {
return Template.instance().azureStats.get();
},
azureStatsLoading() {
return Template.instance().azureStatsLoading.get();
},
azureStatsError() {
return Template.instance().azureStatsError.get();
},
hasGcsStats() {
return !!Template.instance().gcsStats.get();
},
gcsStats() {
return Template.instance().gcsStats.get();
},
gcsStatsLoading() {
return Template.instance().gcsStatsLoading.get();
},
gcsStatsError() {
return Template.instance().gcsStatsError.get();
},
s3Enabled() {
return process.env.S3_ENABLED === 'true';
},
@@ -502,14 +656,60 @@ Template.attachments.events({
'change input.js-toggle-filesystem-read'(event, tpl) {
updateStorageConfigField(tpl, 'filesystem', 'read', event.currentTarget.checked, event.currentTarget);
},
'change input.js-toggle-filesystem-write'(event, tpl) {
updateStorageConfigField(tpl, 'filesystem', 'write', event.currentTarget.checked, event.currentTarget);
},
'change input.js-toggle-gridfs-read'(event, tpl) {
updateStorageConfigField(tpl, 'gridfs', 'read', event.currentTarget.checked, event.currentTarget);
},
'change input.js-toggle-gridfs-write'(event, tpl) {
updateStorageConfigField(tpl, 'gridfs', 'write', event.currentTarget.checked, event.currentTarget);
'click button.js-save-default-storage'(event, tpl) {
event.preventDefault();
const storageName = tpl.$('.js-default-save-storage').val();
if (!storageName) return;
Meteor.call('setDefaultAttachmentStorage', storageName, (error) => {
if (error) {
alert(`${TAPi18n.__('default-save-storage-save-failed')}: ${error.reason || error.message}`);
return;
}
alert(TAPi18n.__('default-save-storage-saved'));
refreshAttachmentStorageSettings(tpl);
});
},
'click button.js-save-cloud-settings'(event, tpl) {
event.preventDefault();
const provider = event.currentTarget.dataset.provider;
if (!provider) return;
const cfg = gatherCloudConfig(tpl, provider);
// Send only this provider's config; the server merges it over the stored
// settings and preserves secrets left blank as well as the other providers.
const nextSettings = { storageConfig: { [provider]: cfg } };
Meteor.call('updateAttachmentStorageSettings', nextSettings, (error) => {
if (error) {
alert(`${TAPi18n.__('cloud-settings-save-failed')}: ${error.reason || error.message}`);
return;
}
alert(TAPi18n.__('cloud-settings-saved'));
refreshAttachmentStorageSettings(tpl);
});
},
'click button.js-test-cloud-connection'(event, tpl) {
event.preventDefault();
const provider = event.currentTarget.dataset.provider;
if (!provider) return;
const cfg = gatherCloudConfig(tpl, provider);
Meteor.call('testAttachmentCloudConnection', provider, cfg, (error, result) => {
const results = { ...tpl.cloudTestResults.get() };
const errors = { ...tpl.cloudTestErrors.get() };
if (error) {
results[provider] = false;
errors[provider] = error.reason || error.message;
} else if (result && result.ok) {
results[provider] = true;
errors[provider] = '';
} else {
results[provider] = false;
errors[provider] = (result && result.error) || 'failed';
}
tpl.cloudTestResults.set(results);
tpl.cloudTestErrors.set(errors);
});
},
'click button.js-calculate-gridfs-stats'(event, tpl) {
event.preventDefault();
@@ -553,6 +753,34 @@ Template.attachments.events({
tpl.s3Stats.set(result || null);
});
},
'click button.js-calculate-azure-stats'(event, tpl) {
event.preventDefault();
tpl.azureStatsLoading.set(true);
tpl.azureStatsError.set('');
Meteor.call('getAzureStorageStats', (error, result) => {
tpl.azureStatsLoading.set(false);
if (error) {
tpl.azureStats.set(null);
tpl.azureStatsError.set(error.reason || error.message || 'Failed to calculate counts');
return;
}
tpl.azureStats.set(result || null);
});
},
'click button.js-calculate-gcs-stats'(event, tpl) {
event.preventDefault();
tpl.gcsStatsLoading.set(true);
tpl.gcsStatsError.set('');
Meteor.call('getGcsStorageStats', (error, result) => {
tpl.gcsStatsLoading.set(false);
if (error) {
tpl.gcsStats.set(null);
tpl.gcsStatsError.set(error.reason || error.message || 'Failed to calculate counts');
return;
}
tpl.gcsStats.set(result || null);
});
},
'click button.js-compact-mongodb-gridfs'(event, tpl) {
event.preventDefault();
runCompact(tpl.compactLoading, tpl.compactResult, tpl.compactError);
@@ -560,83 +788,79 @@ Template.attachments.events({
});
Template.moveAttachments.onCreated(function () {
this.attachments = null;
this.moveProgressData = new ReactiveVar(null);
this._moveController = null;
this.moveCompactLoading = new ReactiveVar(false);
this.moveCompactResult = new ReactiveVar(null);
this.moveCompactError = new ReactiveVar('');
// The bulk move runs as a server-side background job; subscribe to its
// persisted progress so it keeps running (and stays visible) even if the
// admin navigates away from or closes this page.
this.bulkMoveSubscription = Meteor.subscribe('attachmentBulkMoveStatus');
this.repairLoading = new ReactiveVar(false);
this.repairResult = new ReactiveVar(null);
this.repairError = new ReactiveVar('');
});
function getBulkMoveProgress() {
const doc = AttachmentBulkMoveStatus.findOne('bulk');
return doc && doc.running ? doc : null;
}
function getLastMove() {
const doc = AttachmentBulkMoveStatus.findOne('bulk');
// Don't show the "last move" line while a move is actively running.
if (!doc || doc.running || !doc.lastMove) return null;
return doc.lastMove;
}
function storageLabel(value) {
return value ? TAPi18n.__(`move-storage-${value}`) : '';
}
function scopeLabel(value) {
return value ? TAPi18n.__(`move-scope-${value}`) : '';
}
// Format a Date (or value) as YYYY-MM-DD HH:MM:SS in local time.
function formatDateTime(value) {
if (!value) return '';
const date = value instanceof Date ? value : new Date(value);
if (isNaN(date.getTime())) return '';
const p = n => String(n).padStart(2, '0');
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ` +
`${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
}
Template.moveAttachments.helpers({
moveCompactLoading() {
return Template.instance().moveCompactLoading.get();
},
moveCompactResult() {
return Template.instance().moveCompactResult.get();
},
moveCompactError() {
return Template.instance().moveCompactError.get();
},
moveCompactResultItems() {
const result = Template.instance().moveCompactResult.get();
if (!result) return [];
const items = [];
for (const [node, nodeData] of Object.entries(result)) {
if (nodeData && typeof nodeData === 'object') {
for (const [collName, status] of Object.entries(nodeData)) {
items.push({ name: `[${node}] ${collName}`, status });
}
} else {
items.push({ name: node, status: String(nodeData) });
}
}
return items;
},
moveProgress() {
return Template.instance().moveProgressData.get();
return getBulkMoveProgress();
},
moveProgressPaused() {
return Template.instance().moveProgressData.get()?.paused === true;
return getBulkMoveProgress()?.paused === true;
},
moveProgressBarStyle() {
const p = Template.instance().moveProgressData.get();
const p = getBulkMoveProgress();
const pct = p && p.total ? Math.round((p.done / p.total) * 100) : 0;
return `width: ${pct}%`;
},
moveProgressSize() {
const p = Template.instance().moveProgressData.get();
const p = getBulkMoveProgress();
return p?.size ? filesize(p.size) : '';
},
getBoardsWithAttachments() {
const tpl = Template.instance();
tpl.attachments = ReactiveCache.getAttachments();
const attachmentsByBoardId = groupBy(tpl.attachments, fileObj => fileObj.meta.boardId);
const ret = Object.keys(attachmentsByBoardId)
.map(boardId => {
const boardAttachments = attachmentsByBoardId[boardId];
boardAttachments.forEach(_attachment => {
_attachment.flatVersion = Object.keys(_attachment.versions)
.map(_versionName => {
const _version = Object.assign(_attachment.versions[_versionName], {"versionName": _versionName});
// Read storage directly from the document (set by onAfterUpload on server)
_version.storageName = _version.storage || (
(_attachment.meta?.source === 'import' || _version.meta?.gridFsFileId) ? 'gridfs' : 'fs'
);
return _version;
});
});
const board = ReactiveCache.getBoard(boardId);
board.attachments = boardAttachments;
return board;
})
return ret;
lastMove() {
return getLastMove();
},
getBoardData(boardid) {
const ret = ReactiveCache.getBoard(boardId);
return ret;
lastMoveText() {
const lm = getLastMove();
if (!lm) return '';
const cancelled = lm.cancelled ? ` (${TAPi18n.__('move-progress-cancel')})` : '';
return `${storageLabel(lm.source)} → ${storageLabel(lm.dest)} ` +
`(${scopeLabel(lm.scope)}) ${formatDateTime(lm.at)}${cancelled}`;
},
repairLoading() {
return Template.instance().repairLoading.get();
},
repairResult() {
return Template.instance().repairResult.get();
},
repairError() {
return Template.instance().repairError.get();
},
});
@@ -654,126 +878,52 @@ function runCompact(loadingVar, resultVar, errorVar) {
});
}
async function runMoveAll(tpl, attachmentsToMove, destStorage) {
const total = attachmentsToMove.length;
const controller = tpl._moveController;
for (let i = 0; i < total; i++) {
if (controller.cancelled) break;
while (controller.paused && !controller.cancelled) {
await new Promise(r => setTimeout(r, 100));
}
if (controller.cancelled) break;
const att = attachmentsToMove[i];
tpl.moveProgressData.set({
total,
done: i,
current: i + 1,
name: att.name || att._id,
size: att.size || 0,
paused: false,
});
await new Promise(resolve => {
Meteor.call('moveAttachmentToStorage', att._id, destStorage, () => resolve());
});
}
tpl.moveProgressData.set(null);
tpl._moveController = null;
}
function attachmentVersionMatchesSource(attachment, source) {
const versions = Object.values(attachment.versions || {});
return versions.some(v => {
switch (source) {
case 'collectionfs':
return !!(v.meta?.gridFsFileId);
case 'gridfs':
return v.storage === 'gridfs' && !v.meta?.gridFsFileId;
case 'fs':
return v.storage === 'fs' || (!v.storage && !v.meta?.gridFsFileId);
case 's3':
return v.storage === 's3';
default:
return false;
}
});
}
Template.moveAttachments.events({
'click button.js-move-all-attachments'(event, tpl) {
if (tpl.moveProgressData.get()) return;
if (getBulkMoveProgress()) return;
const scope = tpl.$('.js-move-scope').val() || 'attachments';
const source = tpl.$('.js-move-source-storage').val();
const dest = tpl.$('.js-move-dest-storage').val();
if (!source || !dest || source === dest) return;
const destStorage = dest === 'collectionfs' ? 'gridfs' : dest;
const toMove = (tpl.attachments || []).filter(a => attachmentVersionMatchesSource(a, source));
if (toMove.length === 0) return;
tpl._moveController = { paused: false, cancelled: false };
runMoveAll(tpl, toMove, destStorage);
},
'click button.js-pause-move'(event, tpl) {
if (!tpl._moveController) return;
tpl._moveController.paused = true;
const p = tpl.moveProgressData.get();
if (p) tpl.moveProgressData.set({ ...p, paused: true });
},
'click button.js-resume-move'(event, tpl) {
if (!tpl._moveController) return;
tpl._moveController.paused = false;
const p = tpl.moveProgressData.get();
if (p) tpl.moveProgressData.set({ ...p, paused: false });
},
'click button.js-cancel-move'(event, tpl) {
if (tpl._moveController) {
tpl._moveController.cancelled = true;
tpl._moveController.paused = false;
}
tpl.moveProgressData.set(null);
tpl._moveController = null;
},
'click button.js-compact-mongodb-move'(event, tpl) {
event.preventDefault();
runCompact(tpl.moveCompactLoading, tpl.moveCompactResult, tpl.moveCompactError);
},
});
Template.moveBoardAttachments.events({
'click button.js-move-all-attachments-of-board'(event, tpl) {
const source = tpl.$('.js-move-board-source-storage').val();
const dest = tpl.$('.js-move-board-dest-storage').val();
if (!source || !dest || source === dest) return;
const destStorage = dest === 'collectionfs' ? 'gridfs' : dest;
const data = Template.currentData();
(data.attachments || []).forEach(_attachment => {
if (attachmentVersionMatchesSource(_attachment, source)) {
Meteor.call('moveAttachmentToStorage', _attachment._id, destStorage);
// 'all' reads from every Read-enabled storage, so it may equal nothing; only
// a specific source must differ from the destination.
if (!source || !dest || (source !== 'all' && source === dest)) return;
// Hand the whole job to the server so the transfer keeps running as a
// background process even if this page is left or closed.
Meteor.call('startBulkAttachmentMove', source, dest, scope, (error, result) => {
if (error) {
if (error.error !== 'bulk-move-already-running') {
alert(error.reason || error.message);
}
return;
}
// No progress bar appears when nothing matches the source, so tell the
// admin instead of silently doing nothing.
if (result && result.total === 0) {
alert(TAPi18n.__('move-attachments-none-found'));
}
});
},
});
Template.moveAttachment.helpers({
fileSize(size) {
const ret = filesize(size);
return ret;
},
});
Template.moveAttachment.events({
'click button.js-move-storage-fs'() {
const data = Template.currentData();
Meteor.call('moveAttachmentToStorage', data._id, "fs");
},
'click button.js-move-storage-gridfs'() {
const data = Template.currentData();
Meteor.call('moveAttachmentToStorage', data._id, "gridfs");
},
'click button.js-move-storage-s3'() {
const data = Template.currentData();
Meteor.call('moveAttachmentToStorage', data._id, "s3");
'click button.js-repair-attachment-locations'(event, tpl) {
if (tpl.repairLoading.get()) return;
tpl.repairLoading.set(true);
tpl.repairResult.set(null);
tpl.repairError.set('');
Meteor.call('repairAttachmentStorageLocations', (error, result) => {
tpl.repairLoading.set(false);
if (error) {
tpl.repairError.set(error.reason || error.message || 'Repair failed');
return;
}
tpl.repairResult.set(result || {});
});
},
'click button.js-pause-move'() {
Meteor.call('pauseBulkAttachmentMove');
},
'click button.js-resume-move'() {
Meteor.call('resumeBulkAttachmentMove');
},
'click button.js-cancel-move'() {
Meteor.call('cancelBulkAttachmentMove');
},
});
+20 -16
View File
@@ -275,12 +275,12 @@ Template.people.helpers({
},
orgList() {
const tpl = Template.instance();
const page = tpl.orgPage.get();
const skipOrgs = (page - 1) * orgsPerPage;
// The 'org' publication already returns only the current page (server-side
// limit/skip, sorted createdAt:-1). Display exactly what it published:
// re-applying skip/limit here paginated an already-paginated set, which
// left page 2 with a single stray doc and later pages empty.
const orgs = Org.find(tpl.findOrgsOptions.get(), {
sort: { orgDisplayName: 1 },
skip: skipOrgs,
limit: orgsPerPage,
sort: { createdAt: -1 },
fields: {
_id: 1,
orgDisplayName: 1,
@@ -295,12 +295,12 @@ Template.people.helpers({
},
teamList() {
const tpl = Template.instance();
const page = tpl.teamPage.get();
const skipTeams = (page - 1) * teamsPerPage;
// The 'team' publication already returns only the current page (server-side
// limit/skip, sorted createdAt:-1). Display exactly what it published:
// re-applying skip/limit here paginated an already-paginated set, which
// left page 2 with a single stray doc and later pages empty.
const teams = Team.find(tpl.findTeamsOptions.get(), {
sort: { teamDisplayName: 1 },
skip: skipTeams,
limit: teamsPerPage,
sort: { createdAt: -1 },
fields: {
_id: 1,
teamDisplayName: 1,
@@ -315,12 +315,13 @@ Template.people.helpers({
},
peopleList() {
const tpl = Template.instance();
const page = tpl.peoplePage.get();
const skipUsers = (page - 1) * usersPerPage;
// The 'people' publication already returns only the current page (server-side
// limit/skip, sorted createdAt:-1). Display exactly what it published:
// re-applying skip/limit here paginated an already-paginated set — that is
// why page 1 showed 25, page 2 showed a single stray doc (the admin's own
// user record, always present in minimongo) and later pages were empty.
const users = Users.find(tpl.findUsersOptions.get(), {
sort: { username: 1 },
skip: skipUsers,
limit: usersPerPage,
sort: { createdAt: -1 },
fields: {
_id: 1,
username: 1,
@@ -394,7 +395,10 @@ Template.people.helpers({
Template.people.events({
'scroll .main-body'(event, tpl) {
if (tpl.peopleSetting.get()) {
// Orgs, teams and people all use explicit prev/next pagination (server-side
// limit/skip driven by orgPage/teamPage/peoplePage). Infinite scroll must
// stay disabled on those tabs so the two paging mechanisms don't fight.
if (tpl.orgSetting.get() || tpl.teamSetting.get() || tpl.peopleSetting.get()) {
return;
}
tpl.infiniteScrolling.checkScrollPosition(event.currentTarget, () => {
+15 -3
View File
@@ -21,6 +21,11 @@
}
.setting-content .content-body {
display: flex;
/* Always keep the menu (left) and content (right) side by side, even on
narrow windows. The side menu has a fixed basis and the main body fills
the rest, so both stay visible without switching to a stacked layout. */
flex-direction: row;
flex-wrap: nowrap;
padding-top: 2vh;
height: 100%;
gap: 1.3vw;
@@ -30,6 +35,8 @@
border: 1px solid #f0f0f0;
border-radius: 0.5vw;
width: min(250px, 32vw);
/* Never let the menu be squeezed away on narrow windows; keep it visible. */
flex: 0 0 auto;
box-shadow: inset -0.2vh -0.2vh 0.4vh rgba(0,0,0,0.05);
}
.setting-content .content-body .side-menu ul li {
@@ -62,9 +69,14 @@
overflow-x: scroll !important;
overflow-y: scroll !important;
scrollbar-gutter: stable;
/* Force horizontal scrollbar to always be visible */
min-width: 100%;
width: 100%;
/* Grow into the space left next to the side menu. flex-basis MUST be 0 here:
this element holds a min-width:1200px table and a 100vw ::after, so a
content-based (auto) basis is huge and, as an overflow:scroll flex item,
resolves unpredictably (collapsing the panel). Basis 0 + grow makes the
width = container - side menu, deterministically. min-width:0 lets it
shrink so the side menu is never pushed out on narrow windows. */
flex: 1 1 0;
min-width: 0;
}
/* Ensure scrollbars are always visible with proper styling for all admin pages */
@@ -26,3 +26,26 @@
margin-top: 1px;
margin-right: 10px;
}
/* Keep all admin tabs (Settings, People, Reports, Attachments, Translation,
Version) visible on narrow screens. The bar is a fixed 40px tall and the
tabs float, so on narrow widths they wrapped below the bar and were clipped
(disappeared). Let the bar grow and wrap the tabs to as many rows as needed
so none are ever hidden. Scoped via :has() so the board header is untouched. */
#header #header-main-bar:has(.setting-header-btns) {
height: auto;
min-height: 40px;
padding-bottom: 6px;
overflow: visible;
}
#header #header-main-bar .setting-header-btns.left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
float: none;
}
#header #header-main-bar .setting-header-btn {
float: none;
margin: 2px 0;
}
+48
View File
@@ -342,3 +342,51 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
padding-top: 5px;
margin-left: 40px;
}
/* Board backgrounds management popup */
.board-backgrounds-manage .board-backgrounds-grid {
display: flex;
flex-wrap: wrap;
margin-top: 8px;
}
.board-bg-item {
width: 48%;
margin: 1%;
border: 2px solid transparent;
border-radius: 3px;
}
.board-bg-item.is-active {
border-color: #3cb500;
}
.board-bg-thumb {
height: 80px;
background-size: cover;
background-position: center;
border-radius: 3px;
position: relative;
}
.board-bg-active-check {
position: absolute;
top: 4px;
right: 4px;
color: #3cb500;
background: rgba(255, 255, 255, 0.85);
border-radius: 50%;
padding: 2px;
}
.board-bg-actions {
display: flex;
justify-content: space-around;
padding: 4px 0;
}
.board-bg-actions a {
cursor: pointer;
color: #4d4d4d;
}
.board-bg-actions a:hover {
color: #000;
}
.js-bg-upload-button {
display: inline-block;
margin-bottom: 8px;
}
+36
View File
@@ -129,6 +129,38 @@ template(name="boardChangeColorPopup")
if isSelected
i.fa.fa-check
template(name="boardBackgroundsPopup")
.board-backgrounds-manage
input.js-bg-upload-input.hide(type="file" name="file" accept="image/*")
a.primary.wide.js-bg-upload-button
i.fa.fa-plus
= ' '
| {{_ 'upload-background'}}
if uploading.get
+spinner
if error.get
.warning {{_ error.get}}
.board-backgrounds-grid.clearfix
each backgrounds
.board-bg-item(class="{{#if isActiveBackground}}is-active{{/if}}")
.board-bg-thumb(style="background-image: url('{{link}}');")
if isActiveBackground
i.fa.fa-check.board-bg-active-check
.board-bg-actions
unless isActiveBackground
a.js-set-board-background(title="{{_ 'set-as-active'}}")
i.fa.fa-check-circle-o
a.js-download-board-background(href="{{link}}?download=true" download="{{name}}" title="{{_ 'download'}}")
i.fa.fa-arrow-down
a.js-delete-board-background(title="{{_ 'delete'}}")
i.fa.fa-trash-o
template(name="deleteBoardBackgroundPopup")
p {{_ 'board-background-delete-pop'}}
button.js-confirm.negate.full(type="submit")
i.fa.fa-trash-o
| {{_ 'delete'}}
template(name="boardChangeBackgroundImagePopup")
form
label
@@ -567,6 +599,10 @@ template(name="boardMenuPopup")
a.js-change-background-image
i.fa.fa-picture-o
| {{_ 'board-change-background-image'}}
li
a.js-manage-board-backgrounds
i.fa.fa-image
| {{_ 'board-backgrounds'}}
//Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
//if currentUser.isBoardAdmin
//
+84
View File
@@ -7,6 +7,8 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
import AccessibilitySettings from '/models/accessibilitySettings';
import Boards from '/models/boards';
import Attachments from '/models/attachments';
import { generateUniversalAttachmentUrl } from '/models/lib/universalUrlGenerator';
import Integrations from '/models/integrations';
import Lists from '/models/lists';
import { BOARD_COLORS } from '/models/metadata/colors';
@@ -347,6 +349,7 @@ Template.boardMenuPopup.events({
},
'click .js-change-board-color': Popup.open('boardChangeColor'),
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
'click .js-manage-board-backgrounds': Popup.open('boardBackgrounds'),
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
'click .js-change-language': Popup.open('changeLanguage'),
'click .js-delete-duplicate-lists': Popup.afterConfirm('deleteDuplicateLists', function() {
@@ -869,6 +872,87 @@ Template.boardChangeBackgroundImagePopup.helpers({
},
});
// Manage the board's stored background images (upload / set active / download /
// delete). Backgrounds are board-level Attachments (meta.boardId, no cardId,
// meta.source === 'board-background') in the default attachments storage.
Template.boardBackgroundsPopup.onCreated(function () {
this.uploading = new ReactiveVar(false);
this.error = new ReactiveVar('');
const board = Utils.getCurrentBoard();
this.boardId = board && board._id;
if (this.boardId) {
this.subscribe('boardBackgrounds', this.boardId);
}
});
Template.boardBackgroundsPopup.helpers({
uploading() {
return Template.instance().uploading;
},
error() {
return Template.instance().error;
},
backgrounds() {
// Raw collection docs don't carry the .link() helper, so compute the URL.
return Attachments.collection
.find({
'meta.boardId': Template.instance().boardId,
'meta.source': 'board-background',
})
.fetch()
.map(att => ({
_id: att._id,
name: att.name,
link: generateUniversalAttachmentUrl(att._id),
}));
},
isActiveBackground() {
const board = Utils.getCurrentBoard();
return board && board.backgroundImageId === this._id;
},
});
Template.boardBackgroundsPopup.events({
'click .js-bg-upload-button'(event, tpl) {
event.preventDefault();
tpl.find('.js-bg-upload-input').click();
},
async 'change .js-bg-upload-input'(event, tpl) {
const file = event.currentTarget.files && event.currentTarget.files[0];
if (!file) return;
tpl.error.set('');
tpl.uploading.set(true);
const uploader = await Attachments.insertAsync(
{
file,
chunkSize: 'dynamic',
meta: { boardId: tpl.boardId, source: 'board-background' },
},
false,
);
uploader.on('end', (err) => {
tpl.uploading.set(false);
if (err) tpl.error.set(err.reason || 'upload-failed');
});
uploader.on('error', (err) => {
tpl.uploading.set(false);
tpl.error.set((err && err.reason) || 'upload-failed');
});
uploader.start();
// allow re-selecting the same file later
event.currentTarget.value = '';
},
async 'click .js-set-board-background'() {
const board = Utils.getCurrentBoard();
await board.setBackgroundImage(this._id);
Utils.setBackgroundImage();
},
'click .js-delete-board-background': Popup.afterConfirm('deleteBoardBackground', function () {
Meteor.call('removeBoardBackground', this._id);
Popup.back();
}),
});
Template.boardInfoOnMyBoardsPopup.onCreated(function() {
this.currentBoard = Utils.getCurrentBoard();
});
@@ -139,6 +139,15 @@ Template.createCustomFieldPopup.helpers({
});
},
// The template iterates `{{#each dropdownItems.get}}`. This component used to
// be a BlazeComponent, where `dropdownItems` resolved to the instance's
// ReactiveVar; after the migration to a plain Template that binding rendered
// nothing (so dropdown options never appeared and were lost on save). Expose
// the ReactiveVar as a helper again so `dropdownItems.get` works.
dropdownItems() {
return Template.instance().dropdownItems;
},
getDropdownItems() {
return getDropdownItems(Template.instance());
},
+13
View File
@@ -163,6 +163,19 @@ function currentListIsInThisSwimlane(swimlaneId) {
function currentCardIsInThisList(listId, swimlaneId) {
const currentCard = Utils.getCurrentCard();
if (!currentCard) return false;
// On desktop a clicked card is shown as a draggable popup via the openCards
// list (rendered once in boardBody). Don't ALSO render it inline here, or the
// card opens twice — the inline copy sits under the popup and becomes visible
// when the popup is dragged away by its titlebar handle. Direct card-URL
// navigation sets only currentCard (not openCards), so those still render
// inline as before.
if (!Utils.isMiniScreen()) {
const openCards = Session.get('openCards') || [];
if (openCards.includes(currentCard._id)) {
return false;
}
}
//const currentUser = ReactiveCache.getCurrentUser();
if (
//currentUser &&
+3 -1
View File
@@ -3,6 +3,7 @@ import '/client/components/cards/cardCustomFields.jade';
import '/client/components/cards/cardDate.jade';
import '/client/components/cards/cardDescription.jade';
import '/client/components/cards/cardDetails.jade';
import '/client/components/cards/cardStickers.jade';
import '/client/components/cards/cardTime.jade';
import '/client/components/cards/checklists.jade';
import '/client/components/cards/inlinedCardDescription.jade';
@@ -16,6 +17,7 @@ import '/client/components/cards/cardCustomFields.js';
import '/client/components/cards/cardDate.js';
import '/client/components/cards/cardDescription.js';
import '/client/components/cards/cardDetails.js';
import '/client/components/cards/cardStickers.js';
import '/client/components/cards/cardTime.js';
import '/client/components/cards/checklists.js';
import '/client/components/cards/labels.js';
@@ -31,6 +33,6 @@ import '/client/components/cards/cardTime.css';
import '/client/components/cards/checklists.css';
import '/client/components/cards/labels.css';
import '/client/components/cards/minicard.css';
import '/client/components/cards/minicardOverrides.css';
import '/client/components/cards/resultCard.css';
import '/client/components/cards/subtasks.css';
+1
View File
@@ -16,6 +16,7 @@ import '/imports/lib/customHeadDefaults';
import '/imports/lib/dateUtils';
import '/imports/lib/secureDOMPurify';
import '/models/attachmentMigrationStatus';
import '/models/attachmentBulkMoveStatus';
import '/imports/cronMigrationClient';
// ----------------------------------------------------------------------------
+85
View File
@@ -0,0 +1,85 @@
// Native one-finger touch scrolling for `.dragscroll` containers.
//
// The @wekanteam/dragscroll library only listens for mouse events
// (mousedown / mousemove / mouseup). On iPad / iPhone Safari finger
// scrolling of those containers works anyway, because they are also native
// overflow scrollers with momentum scrolling. But on Fairphone 4 postmarketOS
// Firefox and on the newer Ubuntu Touch Morph Browser, dragging a finger over
// a `.dragscroll` container (All Boards list, board canvas, swimlanes, lists,
// calendar) did not scroll it, because those browsers do not emit the
// synthetic mouse events dragscroll relies on.
//
// This module adds real touch handling so a one-finger drag scrolls the
// nearest `.dragscroll` ancestor on every touch browser. It mirrors the
// behaviour of dragscroll's mouse handler (same `.nodragscroll` / form-field
// exclusions) and is delegated on `document`, so it also covers `.dragscroll`
// elements that are added or replaced reactively without needing a reset.
//
// iOS is intentionally skipped: there finger scrolling already works natively
// (with momentum), and taking it over with preventDefault would only remove
// that momentum.
const isIOS = /iP(hone|ad|od)/.test(navigator.userAgent);
let active = null;
function isExcluded(target) {
return !!(
target.closest &&
target.closest(
// Form fields and editors must keep their own touch behaviour, and
// `.nodragscroll` opts an element out explicitly. `.handle` (the
// swimlane/list drag handles, .js-swimlane-header-handle /
// .js-list-handle) must be excluded too: those start a jQuery UI
// sortable reorder via touch-punch, and hijacking the touch to scroll
// here would block dragging the handle to reorder swimlanes/lists.
'input, select, textarea, .note-editable, .nodragscroll, .handle, .ui-sortable-handle',
)
);
}
function canScroll(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
function onTouchStart(e) {
active = null;
if (e.touches.length !== 1) return;
const target = e.target;
if (isExcluded(target)) return;
const el = target.closest && target.closest('.dragscroll');
if (!el || !canScroll(el)) return;
const touch = e.touches[0];
active = {
el,
startX: touch.clientX,
startY: touch.clientY,
scrollLeft: el.scrollLeft,
scrollTop: el.scrollTop,
};
}
function onTouchMove(e) {
if (!active || e.touches.length !== 1) return;
const touch = e.touches[0];
const el = active.el;
el.scrollLeft = active.scrollLeft - (touch.clientX - active.startX);
el.scrollTop = active.scrollTop - (touch.clientY - active.startY);
// We own the gesture: suppress page rubber-banding and the synthetic mouse
// events that would otherwise double-scroll through dragscroll.
if (e.cancelable) e.preventDefault();
}
function onTouchEnd() {
active = null;
}
// Guard so importing this module from more than one place (boardBody,
// boardsList, ...) still only attaches the listeners once.
if (!isIOS && typeof document !== 'undefined' && !window.__wekanDragscrollTouch) {
window.__wekanDragscrollTouch = true;
document.addEventListener('touchstart', onTouchStart, { passive: true });
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd, { passive: true });
document.addEventListener('touchcancel', onTouchEnd, { passive: true });
}
+17 -2
View File
@@ -84,7 +84,7 @@ window.Popup = new (class {
hasPopupParent: clickFromPopup(evt),
title: self._getTitle(popupName),
depth: self._stack.length,
offset: self._getOffset(openerElement),
offset: self._getOffset(openerElement, popupName),
dataContext: (this && this.currentData && this.currentData()) || (options && options.dataContextIfCurrentDataIsUndefined) || this,
});
@@ -217,7 +217,7 @@ window.Popup = new (class {
// We automatically calculate the popup offset from the reference element
// position and dimensions. We also reactively use the window dimensions to
// ensure that the popup is always visible on the screen.
_getOffset(element) {
_getOffset(element, popupName) {
const $element = $(element);
return () => {
Utils.windowResizeDep.depend();
@@ -230,6 +230,21 @@ window.Popup = new (class {
// Calculate actual popup width based on CSS: min(380px, 55vw)
const popupWidth = Math.min(380, viewportWidth * 0.55);
// Card details popup: dock it to the top of the viewport so it overlays
// the global and board header bars (its z-index already sits above them)
// instead of opening downward from the clicked minicard.
if (popupName === 'cardDetailsPopup') {
const cardWidth = Math.min(600, viewportWidth * 0.8);
const centeredLeft = (viewportWidth - cardWidth) / 2;
// top is forced to 0 in CSS (.pop-over[data-popup="cardDetailsPopup"])
// so the card overlays the header bars; use the full viewport height.
return {
left: Math.max(viewportPadding, centeredLeft),
top: 0,
maxHeight: viewportHeight,
};
}
// If the opener element is missing (e.g., programmatic open), fallback to viewport origin
if (!$element || $element.length === 0) {
return {
+34 -6
View File
@@ -99,9 +99,27 @@ export const Utils = {
return user.profile.mobileMode;
}
// Default to mobile mode for iPhone/iPod
const isIPhone = /iPhone|iPod/i.test(navigator.userAgent);
return isIPhone;
// No explicit preference: pick a sensible default from the device.
// Previously this returned true ONLY for iPhone/iPod, so every other phone
// (Fairphone 4 postmarketOS Firefox, Ubuntu Touch Morph, Android Firefox,
// ...) defaulted to desktop-mode. desktop-mode applies large desktop sizing
// (see .desktop-mode rules in boardHeader.css) which on a small phone screen
// looks roughly 2x too big. Default to mobile-mode for any phone-sized touch
// device, not just iPhone, so non-iPhone phones render at the correct size.
const ua = navigator.userAgent;
const isIPhone = /iPhone|iPod/i.test(ua);
const isPhoneUA =
/Mobile|Android|BlackBerry|IEMobile|Opera Mini/i.test(ua) &&
!/iPad/i.test(ua);
const isUbuntuTouch = /Ubuntu/i.test(ua);
const hasTouch =
('maxTouchPoints' in navigator && navigator.maxTouchPoints > 0) ||
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
// A small touch screen (phone-sized) — covers touch browsers whose UA does
// not contain a phone token. Desktop browsers (no touch, or wide) stay in
// desktop-mode.
const isSmallTouchScreen = hasTouch && window.innerWidth <= 800;
return isIPhone || isPhoneUA || isUbuntuTouch || isSmallTouchScreen;
},
setMobileMode(enabled) {
@@ -171,9 +189,19 @@ export const Utils = {
boardCanvas.style.height = '';
}
} else {
// Desktop mode: apply zoom to entire board-wrapper as before
boardWrapper.style.transform = `scale(${level})`;
boardWrapper.style.transformOrigin = 'top left';
// Desktop mode: apply zoom to entire board-wrapper as before.
// At 100% do NOT set transform: scale(1): any transform makes the
// board-wrapper the containing block + stacking context for its
// position:fixed descendants (the opened/dragged card), which traps the
// card inside the board area below the page header bars. Clearing it at
// 100% lets the opened card overlay the top bars.
if (level === 1) {
boardWrapper.style.transform = '';
boardWrapper.style.transformOrigin = '';
} else {
boardWrapper.style.transform = `scale(${level})`;
boardWrapper.style.transformOrigin = 'top left';
}
// If zoom is 50% or lower, make board wrapper full width like content
if (level <= 0.5) {
+155
View File
@@ -0,0 +1,155 @@
# WeKan multi-tenant deployment on one Linux host
#
# Use this compose file when you want to run several WeKan instances
# (one per customer / domain) on the SAME Linux host, all sharing the
# host's kernel network namespace (network_mode: "host") and one
# MongoDB replica set, with separate logical databases per tenant.
#
# This file shows the minimal extra configuration needed for the
# `uws` DDP transport plus `changeStreams` reactivity to work
# correctly in this topology, namely:
#
# - DDP_TRANSPORT=uws on every service
# - METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling on every service
# - METEOR_SETTINGS with a DISTINCT uws.port per service (5001, 5002, …)
#
# Why the distinct uws.port: the uws DDP transport runs an internal
# WebSocket proxy server on its own TCP port (default 127.0.0.1:5001)
# and forwards WS upgrades from the public PORT into it. When two
# services share a kernel netns and both default to 127.0.0.1:5001,
# the second one to start fails to bind because the listen socket is
# opened exclusively. Give each service its own internal port via
# METEOR_SETTINGS and they coexist cleanly.
#
# Reference: see docs/Platforms/FOSS/Docker/Meteor3/multitenancy.md
# and https://docs.meteor.com/performance/ddp-transport#multitenancy
#
# Setup:
# docker compose -f docker-compose-multitenancy.yml up -d
#
# Tear down:
# docker compose -f docker-compose-multitenancy.yml down -v
#
# Adding more tenants:
# Copy a tenant block, change the service name, PORT, ROOT_URL,
# MONGO_URL database name, and the uws.port inside METEOR_SETTINGS
# to a fresh value (5003, 5004, …). The MongoDB service and the
# reverse proxy in front of WeKan don't need any change.
version: '3.8'
services:
# ---------------------------------------------------------------------
# Shared MongoDB 7 with replica set (required for changeStreams).
# All tenants connect to this instance using DIFFERENT logical
# databases (?authSource if you enable auth).
# ---------------------------------------------------------------------
wekandb:
image: mongo:7
container_name: wekan-db
restart: always
network_mode: "host"
entrypoint: ["sh", "-c"]
command:
- |
mongod --storageEngine wiredTiger --wiredTigerCacheSizeGB 1 --timeZoneInfo /usr/share/zoneinfo --oplogSize 2048 --replSet rs0 --bind_ip 127.0.0.1 --port 27017 --quiet &
until mongosh --host 127.0.0.1 --port 27017 --quiet --eval 'try { const s = rs.status(); quit(s.ok === 1 ? 0 : 1); } catch (e) { if (e.codeName === "NotYetInitialized" || /no replset config has been received|not yet initialized/.test(e.message)) { rs.initiate({_id:"rs0", members:[{_id:0, host:"127.0.0.1:27017"}]}); quit(1); } quit(1); }' >/dev/null 2>&1; do
sleep 2
done
wait
# Report healthy only once the replica set has an elected primary, so the
# tenant apps (depends_on: condition: service_healthy) do not start before
# MongoDB is ready and crash with "Topology is closed". start_period gives a
# large database time to replay its journal after an upgrade.
healthcheck:
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
interval: 10s
timeout: 10s
retries: 30
start_period: 60s
volumes:
- wekan-db:/data/db
- wekan-db-dump:/dump
# ---------------------------------------------------------------------
# Tenant 1
#
# Each tenant needs three things distinct from every other tenant:
# 1. PORT — public HTTP port (here 8081)
# 2. MONGO_URL database name — here `wekan_tenant1`
# 3. uws.port in METEOR_SETTINGS — here 5001
# ---------------------------------------------------------------------
wekan-tenant1:
image: ghcr.io/wekan/wekan:latest
container_name: wekan-tenant1
restart: always
network_mode: "host"
depends_on:
wekandb:
condition: service_healthy
environment:
- PORT=8081
- ROOT_URL=https://tenant1.example.com
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant1?replicaSet=rs0
- MONGO_OPLOG_URL=mongodb://127.0.0.1:27017/local?replicaSet=rs0
- DDP_TRANSPORT=uws
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
# Internal uws proxy port — MUST be distinct per service.
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
- WRITABLE_PATH=/data
- WITH_API=true
volumes:
- wekan-tenant1-data:/data
# ---------------------------------------------------------------------
# Tenant 2 — same template, distinct PORT / database / uws.port
# ---------------------------------------------------------------------
wekan-tenant2:
image: ghcr.io/wekan/wekan:latest
container_name: wekan-tenant2
restart: always
network_mode: "host"
depends_on:
wekandb:
condition: service_healthy
environment:
- PORT=8082
- ROOT_URL=https://tenant2.example.com
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant2?replicaSet=rs0
- MONGO_OPLOG_URL=mongodb://127.0.0.1:27017/local?replicaSet=rs0
- DDP_TRANSPORT=uws
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5002,"host":"127.0.0.1"}}}}
- WRITABLE_PATH=/data
- WITH_API=true
volumes:
- wekan-tenant2-data:/data
# ---------------------------------------------------------------------
# Add more tenants by copying a block above and bumping:
# PORT (e.g. 8083, 8084, …)
# ROOT_URL (the tenant's public domain)
# MONGO_URL (a new database name)
# uws.port (5003, 5004, …)
# ---------------------------------------------------------------------
volumes:
wekan-db:
wekan-db-dump:
wekan-tenant1-data:
wekan-tenant2-data:
# ---------------------------------------------------------------------
# Verifying the uws internal listen sockets are isolated.
#
# From the host (or any container sharing the netns):
#
# cat /proc/net/tcp | awk '$4 == "0A" {print $2}' | sort | uniq -c
#
# You want to see ONE listener per uws port — the hex addresses
# `0100007F:1389` (5001) and `0100007F:138A` (5002) should each
# appear with count 1, no duplicates. If you see `2 0100007F:1389`,
# two services are trying to share the default port; reconfigure
# their METEOR_SETTINGS.uws.port to distinct values and restart.
# ---------------------------------------------------------------------
+36 -5
View File
@@ -258,6 +258,17 @@ services:
- wekan-tier
expose:
- 27017
# Healthcheck: report healthy only once the replica set has an elected
# primary, so the wekan app (depends_on: condition: service_healthy) does
# not start before MongoDB is ready and crash with "Topology is closed".
# start_period gives a large database time to replay its journal after an
# upgrade before failures are counted.
healthcheck:
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
interval: 10s
timeout: 10s
retries: 30
start_period: 60s
volumes:
- /etc/localtime:/etc/localtime:ro
- wekan-db:/data/db
@@ -315,9 +326,9 @@ services:
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
# Use oplog,polling to fix performance until changeStreams is fixed at next release of Meteor 3.5 Beta:
# https://github.com/wekan/wekan/issues/6307#issuecomment-4299349231
- METEOR_REACTIVITY_ORDER=oplog,polling
- DDP_TRANSPORT=sockjs
#- DDP_TRANSPORT=uws
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- DDP_TRANSPORT=uws
#- DDP_TRANSPORT=sockjs
#-----------------------------------------------------------------
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
- WRITABLE_PATH=/data
@@ -793,7 +804,15 @@ services:
# The limit number of entries (0=unlimited)
#- LDAP_SEARCH_SIZE_LIMIT=0
#
# Enable group filtering. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap.
# Enable the login restriction group filter. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap.
# When true, only members of LDAP_GROUP_FILTER_GROUP_NAME are allowed to log in.
# NOTE: This flag ONLY controls the login restriction. Admin status sync
# (LDAP_SYNC_ADMIN_STATUS / LDAP_SYNC_ADMIN_GROUPS) and group->role sync
# (LDAP_SYNC_GROUP_ROLES) query LDAP groups independently and do NOT require
# this flag to be true. The group filter metadata below
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
# must still be configured for any group search to work.
#- LDAP_GROUP_FILTER_ENABLE=false
#
# The object class for filtering. Example: group
@@ -847,6 +866,12 @@ services:
# with the LDAP_SYNC_USER_DATA_FIELDMAP is defined in setting LDAP_DEFAULT_DOMAIN above.
#
# Enable/Disable syncing of admin status based on ldap groups:
# NOTE: Admin status sync and group->role sync (LDAP_SYNC_GROUP_ROLES) query
# LDAP groups on their own. They no longer require LDAP_GROUP_FILTER_ENABLE=true,
# which only controls the login restriction filter. The group filter metadata
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
# must still be configured for the group search to work.
#- LDAP_SYNC_ADMIN_STATUS=true
#
# Comma separated list of admin group names to sync.
@@ -902,7 +927,13 @@ services:
# Wait spinner to use
# - WAIT_SPINNER=Bounce
#---------------------------------------------------------------------
#depends_on:
# Wait for MongoDB to be healthy (replica-set primary elected) before
# starting WeKan, so index creation does not crash with "Topology is closed".
# If you use FerretDB instead, comment out the wekandb block below and
# uncomment the wekan-ferretdb block.
depends_on:
wekandb:
condition: service_healthy
# wekan-migrate:
# condition: service_completed_successfully
# wekan-ferretdb:
+9 -1
View File
@@ -378,7 +378,15 @@ services:
# LDAP_SEARCH_SIZE_LIMIT : The limit number of entries (0=unlimited)
# example : LDAP_SEARCH_SIZE_LIMIT=12345
- LDAP_SEARCH_SIZE_LIMIT=0
# LDAP_GROUP_FILTER_ENABLE : Enable group filtering
# LDAP_GROUP_FILTER_ENABLE : Enable the login restriction group filter.
# When true, only members of LDAP_GROUP_FILTER_GROUP_NAME are allowed to log in.
# NOTE: This flag ONLY controls the login restriction. Admin status sync
# (LDAP_SYNC_ADMIN_STATUS / LDAP_SYNC_ADMIN_GROUPS) and group->role sync
# (LDAP_SYNC_GROUP_ROLES) query LDAP groups independently and do NOT require
# this flag to be true. The group filter metadata below
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
# must still be configured for any group search to work.
# example : LDAP_GROUP_FILTER_ENABLE=true
- LDAP_GROUP_FILTER_ENABLE=false
# LDAP_GROUP_FILTER_OBJECTCLASS : The object class for filtering
@@ -31,14 +31,13 @@
# - WeKan hosting CloudFlare DNS: customer.wekan.team A Server-IPv4-Address
wekan.customer.com {
# b) No custom domain, using customer.wekan.team:
# - Using CloudFlare TLS Origin Certificate with Strict Enforcement
# - At CloudFlare DNS change to orange cloud, with CloudFlare proxy,
# - https://github.com/wekan/wekan/blob/main/docs/Webserver/Caddy.md
# - Add this tls section to this example:
# tls {
# load /etc/caddy/certs
# }
# b) No custom domain, using customer.wekan.team:
# - Using CloudFlare TLS Origin Certificate with Strict Enforcement
# - At CloudFlare DNS change to orange cloud, with CloudFlare proxy,
# - https://github.com/wekan/wekan/blob/main/docs/Webserver/Caddy.md
# tls {
# load /etc/caddy/certs
# }
header {
-Content-Security-Policy-Report-Only
-Content-Security-Policy
@@ -54,7 +53,7 @@ wekan.customer.com {
header Access-Control-Allow-Origin *
}
handle {
reverse_proxy 127.0.0.1:3039
reverse_proxy 127.0.0.1:3001
}
}
@@ -65,16 +64,16 @@ wekan.customer.com {
# - Difference is additional tls section: tls { load /etc/caddy/certs }
customer.wekan.team {
tls {
load /etc/caddy/certs
}
tls {
load /etc/caddy/certs
}
header {
-Content-Security-Policy-Report-Only
-Content-Security-Policy
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://static.cloudflareinsights.com https://accounts.google.com https://apis.google.com; worker-src 'self' blob:; connect-src 'self' wss://customer.wekan.team https://customer.wekan.team https://cloudflareinsights.com https://accounts.google.com; img-src 'self' data: https://*.googleusercontent.com; frame-src 'self' https://accounts.google.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:;"
Cross-Origin-Opener-Policy "same-origin-allow-popups"
X-Content-Type-Options nosniff
Strict-Transport-Security "max-age=31536000;"
Strict-Transport-Security "max-age=31536000;"
-X-Frame-Options
Cache-Control "no-cache, no-store, must-revalidate"
}
@@ -82,9 +81,10 @@ customer.wekan.team {
header Content-Type application/json
header Access-Control-Allow-Origin *
}
handle {
reverse_proxy 127.0.0.1:3040
}
handle {
reverse_proxy 127.0.0.1:3002
}
}
# 3) Static website
@@ -247,6 +247,7 @@ boards.wekan.team {
tls {
load /etc/caddy/certs
}
header {
-Content-Security-Policy-Report-Only
-Content-Security-Policy
@@ -433,8 +434,8 @@ boards.wekan.team {
"prefer_related_applications": false
}`
}
handle {
reverse_proxy 127.0.0.1:3025
}
}
@@ -1,102 +1,96 @@
# Multitenancy
TLDR a) and b)
Run several WeKan instances (one per customer / domain) on the same
Linux host. Two supported topologies, pick whichever fits your
deployment.
## a) Oplog sockjs
## a) Recommended — one host, many WeKan domains, `uws` + `changeStreams`
- One server/Many WeKan domains/One IPv4
- Multitenancy works
Highest DDP throughput and lowest reactive latency.
docker-compose.yml:
Configure each instance with:
- `DDP_TRANSPORT=uws`
- `METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling`
- A **distinct** `Meteor.settings.packages['ddp-server'].uws.port`
per instance (`5001`, `5002`, `5003`, … in the example below)
- `network_mode: "host"` (or any setup that shares one kernel
network namespace)
- The same MongoDB replica set (separate logical databases per
tenant)
Why the distinct `uws.port`: the `uws` DDP transport runs its own
internal WebSocket proxy server on a dedicated TCP port (default
`127.0.0.1:5001`) and forwards public WebSocket upgrades into it.
The internal listen socket is opened exclusively, so each instance
in the same kernel netns must declare its own internal port.
Working example: see [`docker-compose-multitenancy.yml`](../../../../../docker-compose-multitenancy.yml)
in the repository root.
Sketch of the per-tenant environment block:
```yaml
environment:
- PORT=8081 # public HTTP port
- ROOT_URL=https://tenant1.example.com
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant1?replicaSet=rs0
- DDP_TRANSPORT=uws
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
```
For tenant 2, bump `PORT`, `ROOT_URL`, the MongoDB database name, and
the `uws.port` (e.g. `5002`). The reverse proxy in front of WeKan
keeps routing customer domains to each tenant's public `PORT`
exactly as before — the internal `uws.port` is never exposed.
### Verifying the configuration
From the host (or from inside any container sharing the netns):
```sh
cat /proc/net/tcp | awk '$4 == "0A" {print $2}' | sort | uniq -c
```
Each `uws.port` should appear exactly once. `0x1389` is `5001`,
`0x138A` is `5002`, etc. If you see `2 0100007F:1389`, two tenants
are trying to share the default port; reconfigure their
`METEOR_SETTINGS.uws.port` to distinct values and restart.
### If you forget to set a distinct `uws.port`
The second tenant to start refuses the bind and Meteor throws at
startup with an actionable error:
```text
Error: uWebSockets.js: failed to listen on 127.0.0.1:5001 (address already in use).
Another Meteor instance in this network namespace is already bound to this port.
Set a distinct Meteor.settings.packages["ddp-server"].uws.port (or .host) for each instance.
```
Add a `METEOR_SETTINGS` line with a free port to the failing
service, restart.
## b) Alternative — `sockjs` + `oplog` (maximum proxy compatibility)
Use this if your reverse proxy / load balancer cannot guarantee
WebSocket upgrade pass-through, or if some clients sit on networks
that block raw WebSocket. SockJS will fall back to HTTP long-polling
on those clients.
```yaml
- DDP_TRANSPORT=sockjs
- METEOR_REACTIVITY_ORDER=oplog,polling
- DDP_TRANSPORT=uws
```
## b) changeStreams uws
No per-tenant `uws.port` configuration is needed because `sockjs`
does not run a separate internal listen socket. The trade-off is
lower DDP throughput compared to `uws`.
- One server/One WeKan domain/One IPv4
## Reference
Multitenancy does not work, causes bug: round robin changing
WeKan website at every webpage reload at same address.
For example:
1) At https://boards.wekan.team shows https://wekan.customer2.com
2) Reload webpage. https://boards.wekan.team shows https://wekan.customer3.com
3) Reload webpage. https://boards.wekan.team shows https://wekan.customer4.com
- 4) Reload webpage. https://boards.wekan.team shows https://customer4.wekan.team
- etc.
## Details
When multiple Meteor 3 applications are running on the same server using
`network_mode: "host"` and are shared across the same MongoDB server,
the combination `DDP_TRANSPORT=uws` and `METEOR_REACTIVITY_ORDER=changeStreams`
is subject to a very rare but critical **Context/Descriptor Confusion**.
Here is a detailed technical explanation of what is happening behind the scenes and why it breaks a multi-tenant environment:
### 1. µWebSockets (`uws`) and shared host networking
`uws` is an extremely high-performance WebSocket engine written in C++.
It bypasses many of the traditional networking layers of Node.js and
handles network sockets and their file handles (File Descriptors)
directly at a low level in its own Event Loop.
When containers are set to `network_mode: "host"`, they are not
network-level isolated. They share the same Linux kernel
network stack and memory space for network traffic.
When your browser does a WebSocket handshake (`Upgrade: websocket`)
after an HTTP request, `uws` allocates a memory address for the socket.
The bug occurs when multiple containers do this at the same time
in the same host network environment,
**the low-level C++ code in the `uws` engine can cross-identify socket identifiers from different processes**
if the framework (in this case, an early beta integration of Meteor 3)
is unable to keep connection ownership (v8 isolation) completely separate.
### 2. `changeStreams` and data misrouting
MongoDB's `changeStreams` relies on the application opening a long-lived,
real-time listening pipe to the database. It is heavier and more complex
than the old `oplog` table because it creates constantly changing,
stateful cursors between the application and the database.
When this is combined with `uws` WebSocket:
1. User A opens the `boards.wekan.team` page.
2. The browser requests a WebSocket connection, and Meteor creates a
`changeStream` listener on the database for User A's container.
3. At the same time, User B loads the `kanban.buetzow.de` page.
4. Since both containers talk locally to the same MongoDB process
and share the same host interface, the application layer's
reactivity engine (multiplexer) confuses which `changeStream`
data input belongs to which active `uws` socket address.
**Result:** A reactive data packet from the database (e.g. different customer 1 boards)
is pushed into the `uws` pipe that was locally open to the `boards.wekan.team` browser.
The browser gets a perfectly valid HTML body, but the Javascript layer draws the data
it received from the WebSocket.
### Why did good old `sockjs` and `oplog` fix the situation?
* **`sockjs`** is written in pure JavaScript (Node.js' native network stack).
It strictly follows Node.js' own process boundaries, and does not perform
direct C++-level optimizations beyond the kernel. Even though containers
share the host network, Node.js makes sure that each process's sockets
remain strictly within its own application window.
* **`oplog`** is a more passive way to read the database. The application
reads the raw log file (`local.oplog.rs`) and filters its own data from
there, instead of MongoDB's active `changeStream` engine trying to route
and load balance cursors between different processes on the fly.
Simply put: `uws` and `changeStreams` are great tools when you are running
**only one large application** on a single server for which you want
maximum performance. In a multi-tenant environment
(many different clients on the same machine), they are too aggressive
and break the invisible boundaries between containers.
- Meteor 3.5 DDP transport docs, including the multi-tenant section:
<https://docs.meteor.com/performance/ddp-transport#multitenancy>
- Meteor changeStreams reactivity driver:
<https://docs.meteor.com/performance/change-streams-observer-driver>
@@ -17,7 +17,7 @@ for d in */ ; do
docker compose up -d --force-recreate
#docker compose stop
#docker compose start
#docker rm "${FOLDER_NAME}-app"
#docker rm "${FOLDER_NAME}-tenant"
#docker compose up -d
# Return to the parent directory
@@ -1,4 +1,15 @@
# ========== CUSTOM END =====
## See ../multitenancy.md about commented out settings:
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- DDP_TRANSPORT=uws
#- METEOR_REACTIVITY_ORDER=oplog,polling
#- DDP_TRANSPORT=sockjs
- WRITABLE_PATH=/data
- WITH_API=true
- RICHER_CARD_COMMENT_EDITOR=false
- CARD_OPENED_WEBHOOK_ENABLED=false
- BIGEVENTS_PATTERN=NONE
- NODE_FLAGS=--max-old-space-size=16384
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
@@ -1,6 +1,9 @@
- MONGO_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/els?authSource=els
- MONGO_OPLOG_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/local?replicaSet=rs0&authSource=els
- PORT=3001
- ROOT_URL=https://wekan.customer.com
- MONGO_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/PARAMETER?replicaSet=rs0&appName=PARAMETER-tenant&authSource=PARAMETER
- MONGO_OPLOG_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/local?replicaSet=rs0&appName=PARAMETER-tenant-oplog&authSource=PARAMETER
# Distinct uws internal port per tenant — required since the
# 3.5-beta.12 fix (PR #14425) binds with LIBUS_LISTEN_EXCLUSIVE_PORT.
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
- MAIL_FROM=Customer WeKan Boards <boards@wekan.team>
- MAIL_URL=smtp://AKIA-USERNAME:PASSWORD@email-smtp.eu-west-1.amazonaws.com:587?tls={ciphers:"SSLv3"}&secureConnection=false
- PORT=3032
- ROOT_URL=https://wekan.customer.com
@@ -2,19 +2,8 @@ services:
PARAMETER:
image: ghcr.io/wekan/wekan:v9.20
container_name: PARAMETER-app
restart: always
container_name: PARAMETER-tenant
restart: unless-stopped
network_mode: "host"
environment:
## See ../multitenancy.md about commented out settings:
#- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
- METEOR_REACTIVITY_ORDER=oplog,polling
#- DDP_TRANSPORT=uws
- DDP_TRANSPORT=sockjs
- WRITABLE_PATH=/data
- WITH_API=true
- RICHER_CARD_COMMENT_EDITOR=false
- CARD_OPENED_WEBHOOK_ENABLED=false
- BIGEVENTS_PATTERN=NONE
- NODE_FLAGS=--max-old-space-size=16384
# ========== CUSTOM START =====
@@ -14,7 +14,7 @@ for d in restore/*/ ; do
# Run the Docker commands using the folder name
docker compose down
docker rm "${FOLDER_NAME}-app"
docker rm "${FOLDER_NAME}-tenant"
docker compose up -d --force-recreate
# Return to the parent directory
@@ -25,7 +25,7 @@ for d in restore/*/ ; do
# Run the Docker commands using the clean folder name
docker compose down
docker rm "${FOLDER_NAME}-app" 2>/dev/null # 2>/dev/null ignores errors if it's already gone
docker rm "${FOLDER_NAME}-tenant" 2>/dev/null # 2>/dev/null ignores errors if it's already gone
docker compose up -d --force-recreate
)
done
+4 -4
View File
@@ -10,19 +10,19 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-9.32-win64.zip](https://github.com/wekan/wekan/releases/download/v9.32/wekan-9.32-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
1. [wekan-9.42-win64.zip](https://github.com/wekan/wekan/releases/download/v9.42/wekan-9.42-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
3. [mongodb-windows-x86_64-7.0.32-signed.msi](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.32-signed.msi)
3. [mongodb-windows-x86_64-7.0.34-signed.msi](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.34-signed.msi)
4. (already exists: [start-wekan.bat](https://raw.githubusercontent.com/wekan/wekan/main/start-wekan.bat))
5. Copy files from steps 1-3 with USB stick or DVD to offline Windows computer
6. Double click `mongodb-windows-x86_64-7.0.32-signed.msi` . In installer, uncheck downloading MongoDB compass.
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-9.32-win64.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-9.42-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
"import-json-placeholder": "Tempelkan data JSON yang sah disini",
"import-csv-placeholder": "Tampal data CSV/TSV yang sah di sini",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Petakan partisipan",
"import-members-map": "Papan yang diimport mempunyai ahli. Sila padankan ahli yang anda mahukan untuk import kepada pengguna anda",
"import-members-map-note": "Ahli yang tidak dipadan akan di padankan kepada pengguna yang ada",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Urutan aktif",
"cardsSortPopup-title": "Susun Kad",
"due-date": "Tarikh Akhir",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Ralat Komputer Pelayan",
"server-error-troubleshooting": "Sila masukkan ralat yang diberikan oleh komputer pelayan. \nUntuk instalasi menggunakan snap, larikan: 'sudo snap logs wekan.wekan'\nUntuk Instalasi menggunakan Docker, larikan: 'sudo docker logs wekan-app'",
"title-alphabetically": "Judul (mengikut abjad)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Daftar Lampiran",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "tipe",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "As jy foute kry wanneer jy bord invoer, werk die invoer soms steeds, en bord is op Alle Borde-bladsy.",
"import-json-placeholder": "Plak jou geldige JSON-data hier",
"import-csv-placeholder": "Plak jou geldige CSV/TSV-data hier",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Kaart lede",
"import-members-map": "Jou ingevoerde raad het 'n paar lede. Karteer asseblief die lede wat jy wil invoer na jou gebruikers toe",
"import-members-map-note": "Let wel: Ongekarte lede sal aan die huidige gebruiker toegewys word.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sorteer is aan",
"cardsSortPopup-title": "Sorteer kaarte",
"due-date": "Sperdatum",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Bedienerfout",
"server-error-troubleshooting": "Dien asseblief die fout in wat deur die bediener gegenereer is.\nVir 'n vinnige installasie, hardloop: `sudo snap logs wekan.wekan`\nVir 'n Docker-installasie, hardloop: `sudo docker logs wekan-app`",
"title-alphabetically": "Titel (alfabeties)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Aanhangsels",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Lêer",
"move-progress-pause": "Pouse",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Tik",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS geaktiveer",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Gebruik MongoDB GridFS vir lêerberging",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Kon nie migrasies stop nie",
"migration-stopped": "Migrasies is suksesvol gestop",
"mongodb-gridfs-storage": "MongoDB GridFS-berging",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Onderbreek alle migrasies",
"s3-access-key": "S3 Toegangssleutel",
"s3-access-key-description": "AWS S3 toegangsleutel vir verifikasie",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "As jy foute kry wanneer jy bord invoer, werk die invoer soms steeds, en bord is op Alle Borde-bladsy.",
"import-json-placeholder": "Plak jou geldige JSON-data hier",
"import-csv-placeholder": "Plak jou geldige CSV/TSV-data hier",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Kaart lede",
"import-members-map": "Jou ingevoerde raad het 'n paar lede. Karteer asseblief die lede wat jy wil invoer na jou gebruikers toe",
"import-members-map-note": "Let wel: Ongekarte lede sal aan die huidige gebruiker toegewys word.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sorteer is aan",
"cardsSortPopup-title": "Sorteer kaarte",
"due-date": "Sperdatum",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Bedienerfout",
"server-error-troubleshooting": "Dien asseblief die fout in wat deur die bediener gegenereer is.\nVir 'n vinnige installasie, hardloop: `sudo snap logs wekan.wekan`\nVir 'n Docker-installasie, hardloop: `sudo docker logs wekan-app`",
"title-alphabetically": "Titel (alfabeties)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Aanhangsels",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Lêer",
"move-progress-pause": "Pouse",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Tik",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS geaktiveer",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Gebruik MongoDB GridFS vir lêerberging",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Kon nie migrasies stop nie",
"migration-stopped": "Migrasies is suksesvol gestop",
"mongodb-gridfs-storage": "MongoDB GridFS-berging",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Onderbreek alle migrasies",
"s3-access-key": "S3 Toegangssleutel",
"s3-access-key-description": "AWS S3 toegangsleutel vir verifikasie",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "رسم خريطة الأعضاء",
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
@@ -1261,6 +1304,32 @@
"sort-is-on": "الفرز قيد التشغيل",
"cardsSortPopup-title": "مرتب‌سازی کارت‌ها",
"due-date": "تاریخ اجرا",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "خطأ بالخادم",
"server-error-troubleshooting": "Merci de soumettre l'erreur générée par le serveur. Pour une installation snap, lancer `sudo snap logs wekan.wekan`. Pour une installation docker, lancer `sudo docker logs wekan-app`",
"title-alphabetically": "تیتر (به صورت الفبا)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "المرفقات",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Fichier",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "النوع",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "رسم خريطة الأعضاء",
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
@@ -1261,6 +1304,32 @@
"sort-is-on": "الفرز قيد التشغيل",
"cardsSortPopup-title": "مرتب‌سازی کارت‌ها",
"due-date": "تاریخ اجرا",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "خطأ بالخادم",
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
"title-alphabetically": "تیتر (به صورت الفبا)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "المرفقات",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "النوع",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "رسم خريطة الأعضاء",
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
@@ -1261,6 +1304,32 @@
"sort-is-on": "الفرز قيد التشغيل",
"cardsSortPopup-title": "مرتب‌سازی کارت‌ها",
"due-date": "تاریخ اجرا",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "خطأ بالخادم",
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
"title-alphabetically": "تیتر (به صورت الفبا)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "المرفقات",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "النوع",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "رسم خريطة الأعضاء",
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
@@ -1261,6 +1304,32 @@
"sort-is-on": "الفرز قيد التشغيل",
"cardsSortPopup-title": "مرتب‌سازی کارت‌ها",
"due-date": "تاریخ اجرا",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "خطأ بالخادم",
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
"title-alphabetically": "تیتر (به صورت الفبا)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "المرفقات",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "النوع",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Aunque obtengas errores cuando importes el tablero, a veces la importación funciona igualmente, y el tablero se encontrará en la página de tableros.",
"import-json-placeholder": "Pega tus datos JSON válidos aquí",
"import-csv-placeholder": "Pega tus datos CSV/TSV válidos aquí",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mapa de miembros",
"import-members-map": "Tu tablero importado tiene algunos miembros. Por favor, mapea los miembros que quieres importar con tus usuarios.",
"import-members-map-note": "Nota: Los miembros no mapeados serán asignados al usuario actual.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Ordenamiento activado",
"cardsSortPopup-title": "Ordenar tarjetas",
"due-date": "Fecha de Vencimiento",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Error del Servidor",
"server-error-troubleshooting": "Por favor, envíe el error generado por el servidor.\nPara la instalación con snap, ejecute: `sudo snap logs wekan.wekan`\nPara la instalación con docker, ejecute: `sudo docker logs wekan-app`",
"title-alphabetically": "Título (Alfabéticamente)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Adjuntos",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Tipo",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Fallo al detener las migraciones",
"migration-stopped": "Las migraciones se han detenido correctamente",
"mongodb-gridfs-storage": "Almacenamiento MongoDB GridFS",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pausar todas las migraciones",
"s3-access-key": "Clave de acceso de S3",
"s3-access-key-description": "Clave de acceso de AWS S3 para autentificación",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Lövhəni idxal edərkən səhvlər alsanız, bəzən idxal hələ də işləyir və lövhə Bütün Lövhələr səhifəsindədir.",
"import-json-placeholder": "Etibarlı JSON datanızı bura yerləşdirin",
"import-csv-placeholder": "Etibarlı CSV/TSV datanızı bura yerləşdirin",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Xəritə üzvləri",
"import-members-map": "İdxal etdiyiniz şuranın bəzi üzvləri var. Xahiş edirik, idxal etmək istədiyiniz üzvləri istifadəçilərinizə xəritələşdirin",
"import-members-map-note": "Qeyd: Xəritəsiz üzvlər cari istifadəçiyə təyin ediləcək.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sort aktivdir",
"cardsSortPopup-title": "Kartları çeşidləyin",
"due-date": "Son tarix",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server xətası",
"server-error-troubleshooting": "Zəhmət olmasa server tərəfindən yaradılan xətanı təqdim edin.\nSürətli quraşdırma üçün işləyin: `sudo snap logs wekan.wekan`\nDocker quraşdırması üçün işləyin: `sudo docker logs wekan-app`",
"title-alphabetically": "Başlıq (Əlifba sırası ilə)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Qoşmalar",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Fayl",
"move-progress-pause": "Fasilə",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Növ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Aktivdir",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Fayl saxlama üçün MongoDB GridFS istifadə edin",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Miqrasiyanı dayandırmaq alınmadı",
"migration-stopped": "Miqrasiya uğurla dayandırıldı",
"mongodb-gridfs-storage": "MongoDB GridFS Saxlama",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Bütün miqrasiyaları dayandırın",
"s3-access-key": "S3 Giriş Açarı",
"s3-access-key-description": "Doğrulama üçün AWS S3 giriş açarı",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Lövhəni idxal edərkən səhvlər alsanız, bəzən idxal hələ də işləyir və lövhə Bütün Lövhələr səhifəsindədir.",
"import-json-placeholder": "Etibarlı JSON datanızı bura yerləşdirin",
"import-csv-placeholder": "Etibarlı CSV/TSV datanızı bura yerləşdirin",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Xəritə üzvləri",
"import-members-map": "İdxal etdiyiniz şuranın bəzi üzvləri var. Xahiş edirik, idxal etmək istədiyiniz üzvləri istifadəçilərinizə xəritələşdirin",
"import-members-map-note": "Qeyd: Xəritəsiz üzvlər cari istifadəçiyə təyin ediləcək.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sort aktivdir",
"cardsSortPopup-title": "Kartları çeşidləyin",
"due-date": "Son tarix",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server xətası",
"server-error-troubleshooting": "Zəhmət olmasa server tərəfindən yaradılan xətanı təqdim edin.\nSürətli quraşdırma üçün işləyin: `sudo snap logs wekan.wekan`\nDocker quraşdırması üçün işləyin: `sudo docker logs wekan-app`",
"title-alphabetically": "Başlıq (Əlifba sırası ilə)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Qoşmalar",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Fayl",
"move-progress-pause": "Fasilə",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Növ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Aktivdir",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Fayl saxlama üçün MongoDB GridFS istifadə edin",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Miqrasiyanı dayandırmaq alınmadı",
"migration-stopped": "Miqrasiya uğurla dayandırıldı",
"mongodb-gridfs-storage": "MongoDB GridFS Saxlama",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Bütün miqrasiyaları dayandırın",
"s3-access-key": "S3 Giriş Açarı",
"s3-access-key-description": "Doğrulama üçün AWS S3 giriş açarı",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Lövhəni idxal edərkən səhvlər alsanız, bəzən idxal hələ də işləyir və lövhə Bütün Lövhələr səhifəsindədir.",
"import-json-placeholder": "Etibarlı JSON datanızı bura yerləşdirin",
"import-csv-placeholder": "Etibarlı CSV/TSV datanızı bura yerləşdirin",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Xəritə üzvləri",
"import-members-map": "İdxal etdiyiniz şuranın bəzi üzvləri var. Xahiş edirik, idxal etmək istədiyiniz üzvləri istifadəçilərinizə xəritələşdirin",
"import-members-map-note": "Qeyd: Xəritəsiz üzvlər cari istifadəçiyə təyin ediləcək.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sort aktivdir",
"cardsSortPopup-title": "Kartları çeşidləyin",
"due-date": "Son tarix",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server xətası",
"server-error-troubleshooting": "Zəhmət olmasa server tərəfindən yaradılan xətanı təqdim edin.\nSürətli quraşdırma üçün işləyin: `sudo snap logs wekan.wekan`\nDocker quraşdırması üçün işləyin: `sudo docker logs wekan-app`",
"title-alphabetically": "Başlıq (Əlifba sırası ilə)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Qoşmalar",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Fayl",
"move-progress-pause": "Fasilə",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Növ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Aktivdir",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Fayl saxlama üçün MongoDB GridFS istifadə edin",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Miqrasiyanı dayandırmaq alınmadı",
"migration-stopped": "Miqrasiya uğurla dayandırıldı",
"mongodb-gridfs-storage": "MongoDB GridFS Saxlama",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Bütün miqrasiyaları dayandırın",
"s3-access-key": "S3 Giriş Açarı",
"s3-access-key-description": "Doğrulama üçün AWS S3 giriş açarı",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
"import-json-placeholder": "Paste your valid JSON data here",
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Map members",
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sort is on",
"cardsSortPopup-title": "Sort Cards",
"due-date": "Due Date",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server Error",
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
"title-alphabetically": "Title (Alphabetically)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Attachments",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Type",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Ако се јаве грешке при уношењу списа то не значи да унос није успео!Проверите јел се спис појавио у одељку „Списи у мојој надлежности“.",
"import-json-placeholder": "Копирайте валидната Ви JSON информация тук",
"import-csv-placeholder": "Овде сместите Ваше исправне CSV/TSV податке",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Мапирај сараднике",
"import-members-map": "Списи које сте унели имају постављену мрежу сарадника. Молим да мапирате сараднике из те мреже на Ваше сараднике",
"import-members-map-note": "Белешка: Немапирани сарадници ће бити додељени на тренутног корисника.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Предмети су сложени",
"cardsSortPopup-title": "Сложи предмете",
"due-date": "По крајњем року",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Грешка на серверу",
"server-error-troubleshooting": "Молим да нам пошаљете извештај о грешци коју је изазвао сервер.\nАко је у питању snap инсталација, покрените: `sudo snap logs wekan.wekan`\nАко је у питању Docker инсталација, покрените: `sudo docker logs wekan-app`",
"title-alphabetically": "По наслову абучним редом",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Прикачени файлове",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Предах",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Тип",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS ради и можете га користити",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Можете користити MongoDB GridFS за складиште предметне грађе",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Није било могуће прекинути поступке обнове оштећених списа",
"migration-stopped": "Поступци обнове оштећених списа су управо заустављени",
"mongodb-gridfs-storage": "MongoDB GridFS складиште",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Предах за све",
"s3-access-key": "S3 приступни кључ",
"s3-access-key-description": "AWS S3 приступни кључ за пријаву",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Si une erreur survient en important le tableau, il se peut que l'import ait fonctionné, et que le tableau se trouve sur la page \"Tous les tableaux\".",
"import-json-placeholder": "Collez ici les données JSON valides",
"import-csv-placeholder": "Déposez ici vos données valides CSV/TSV",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Assigner des participants",
"import-members-map": "Le tableau que vous venez d'importer contient des participants. Veuillez assigner les participants que vous souhaitez importer à vos utilisateurs.",
"import-members-map-note": "Note: les participants ne concordant pas seront assignés à l'utilisateur courant.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Le tri est activé",
"cardsSortPopup-title": "Trier les cartes",
"due-date": "Date d'échéance",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Erreur serveur",
"server-error-troubleshooting": "Merci de soumettre l'erreur générée par le serveur. Pour une installation snap, lancer `sudo snap logs wekan.wekan`. Pour une installation docker, lancer `sudo docker logs wekan-app`",
"title-alphabetically": "Titre (Alphabétiquement)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Pièces jointes",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Fichier",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Type",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Si obteniu errors en importar el tauler, de vegades la importació encara funciona, i el tauler es troba a la pàgina Tots els taulers.",
"import-json-placeholder": "Aferra codi JSON vàlid aquí",
"import-csv-placeholder": "Enganxeu aquí les vostres dades CSV/TSV vàlides",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mapeja els membres",
"import-members-map": "El vostre tauler importat té alguns membres. Assigna els membres que vols importar als teus usuaris",
"import-members-map-note": "Nota: els membres no assignats s'assignaran a l'usuari actual.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sort is on",
"cardsSortPopup-title": "Ordenar fitxes",
"due-date": "Data de venciment",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Error del servidor",
"server-error-troubleshooting": "Envieu l'error generat pel servidor.\nPer a una instal·lació instantània, executeu: `sudo snap logs wekan.wekan`\nPer a una instal·lació de Docker, executeu: `sudo docker logs wekan-app`",
"title-alphabetically": "Títol (alfabèticament)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Adjunts",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Tipus",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Si obteniu errors en importar el tauler, de vegades la importació encara funciona, i el tauler es troba a la pàgina Tots els taulers.",
"import-json-placeholder": "Aferra codi JSON vàlid aquí",
"import-csv-placeholder": "Enganxeu aquí les vostres dades CSV/TSV vàlides",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mapeja els membres",
"import-members-map": "El vostre tauler importat té alguns membres. Assigna els membres que vols importar als teus usuaris",
"import-members-map-note": "Nota: els membres no assignats s'assignaran a l'usuari actual.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Ordenamiento activado",
"cardsSortPopup-title": "Ordenar fitxes",
"due-date": "Data de venciment",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Error del servidor",
"server-error-troubleshooting": "Envieu l'error generat pel servidor.\nPer a una instal·lació instantània, executeu: `sudo snap logs wekan.wekan`\nPer a una instal·lació de Docker, executeu: `sudo docker logs wekan-app`",
"title-alphabetically": "Títol (alfabèticament)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Adjunts",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Tipus",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Fallo al detener las migraciones",
"migration-stopped": "Las migraciones se han detenido correctamente",
"mongodb-gridfs-storage": "Almacenamiento MongoDB GridFS",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pausar todas las migraciones",
"s3-access-key": "Clave de acceso de S3",
"s3-access-key-description": "Clave de acceso de AWS S3 para autentificación",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Si obteniu errors en importar el tauler, de vegades la importació encara funciona, i el tauler es troba a la pàgina Tots els taulers.",
"import-json-placeholder": "Aferra codi JSON vàlid aquí",
"import-csv-placeholder": "Enganxeu aquí les vostres dades CSV/TSV vàlides",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mapeja els membres",
"import-members-map": "El vostre tauler importat té alguns membres. Assigna els membres que vols importar als teus usuaris",
"import-members-map-note": "Nota: els membres no assignats s'assignaran a l'usuari actual.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sort is on",
"cardsSortPopup-title": "Ordenar fitxes",
"due-date": "Data de venciment",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Error del servidor",
"server-error-troubleshooting": "Envieu l'error generat pel servidor.\nPer a una instal·lació instantània, executeu: `sudo snap logs wekan.wekan`\nPer a una instal·lació de Docker, executeu: `sudo docker logs wekan-app`",
"title-alphabetically": "Títol (alfabèticament)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Adjunts",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Tipus",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "如果导入板卡时出现错误,有时导入仍然有效,并且板卡位于“所有板卡”页面。",
"import-json-placeholder": "将有效的 JSON 数据粘贴到此处",
"import-csv-placeholder": "在此处粘贴您的有效 CSV/TSV 数据",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "地图成员",
"import-members-map": "您导入的董事会有一些成员。请将您要导入的成员映射到您的用户",
"import-members-map-note": "注意:未映射的成员将分配给当前用户。",
@@ -1261,6 +1304,32 @@
"sort-is-on": "排序已开启",
"cardsSortPopup-title": "卡片排序",
"due-date": "到期日",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "服务器错误",
"server-error-troubleshooting": "请提交服务器产生的错误。\n对于快照安装,请运行:`sudo snap logs wekan.wekan`\n对于 Docker 安装,请运行:`sudo docker logs wekan-app`",
"title-alphabetically": "标题(按字母顺序)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "附件",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "文件",
"move-progress-pause": "暂停",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "类型",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "启用 GridFS",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "使用MongoDB GridFS进行文件存储",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "无法停止迁移",
"migration-stopped": "迁移已成功停止",
"mongodb-gridfs-storage": "MongoDB GridFS 存储",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "暂停所有迁移",
"s3-access-key": "S3 访问密钥",
"s3-access-key-description": "用于身份验证的 AWS S3 访问密钥",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Někdy import funguje i přestože dostáváte chyby při importování tabla, které se zobrazí na stránce Všechna tabla.",
"import-json-placeholder": "Sem vlož validní JSON data",
"import-csv-placeholder": "Zde vložte vaše platné CSV/TSV data",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mapovat členy",
"import-members-map": "Toto importované tablo obsahuje několik osob. Prosím namapujte osoby z importu na místní uživatelské účty.",
"import-members-map-note": "Modifikováno",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Řazení je zapnuté",
"cardsSortPopup-title": "Třídit",
"due-date": "Požadovaný termín",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Chyba na serveru",
"server-error-troubleshooting": "Odešlete prosím chybu, kterou server vygeneroval.\nV případě instalace přes snap spusťte: `sudo snap logs wekan.wekan`\nV případě instalace přes Docker spusťte: `sudo docker logs wekan-app`",
"title-alphabetically": "Nadpis (Abecedně)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Přílohy",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Typ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Někdy import funguje i přestože dostáváte chyby při importování tabla, které se zobrazí na stránce Všechna tabla.",
"import-json-placeholder": "Sem vlož validní JSON data",
"import-csv-placeholder": "Zde vložte vaše platné CSV/TSV data",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mapovat členy",
"import-members-map": "Toto importované tablo obsahuje několik osob. Prosím namapujte osoby z importu na místní uživatelské účty.",
"import-members-map-note": "Modifikováno",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Řazení je zapnuté",
"cardsSortPopup-title": "Třídit",
"due-date": "Požadovaný termín",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Chyba na serveru",
"server-error-troubleshooting": "Odešlete prosím chybu, kterou server vygeneroval.\nV případě instalace přes snap spusťte: `sudo snap logs wekan.wekan`\nV případě instalace přes Docker spusťte: `sudo docker logs wekan-app`",
"title-alphabetically": "Nadpis (Abecedně)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Přílohy",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "File",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Typ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Enabled",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Failed to stop migrations",
"migration-stopped": "Migrations stopped successfully",
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pause All Migrations",
"s3-access-key": "S3 Access Key",
"s3-access-key-description": "AWS S3 access key for authentication",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Os cewch wallau wrth fewnforio bwrdd, weithiau mae mewnforio yn dal i weithio, ac mae bwrdd ar dudalen All Boards.",
"import-json-placeholder": "Gludwch eich data JSON dilys yma",
"import-csv-placeholder": "Gludwch eich data CSV/TSV dilys yma",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Aelodau map",
"import-members-map": "Mae gan eich bwrdd wedi'i fewnforio rai aelodau. Mapiwch yr aelodau yr ydych am eu mewnforio i'ch defnyddwyr",
"import-members-map-note": "Nodyn: Bydd aelodau heb eu mapio yn cael eu neilltuo i'r defnyddiwr presennol.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Mae trefn ymlaen",
"cardsSortPopup-title": "Cardiau Didoli",
"due-date": "Dyddiad Cwblhau",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Gwall Gweinydd",
"server-error-troubleshooting": "Cyflwynwch y gwall a gynhyrchwyd gan y gweinydd.\nAr gyfer gosodiad snap, rhedwch: `sudo snap logs wekan.wekan`\nAr gyfer gosodiad Docker, rhedwch: `sudo docker logs wekan-app`",
"title-alphabetically": "Teitl (Yn nhrefn yr wyddor)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Ymlyniadau",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Ffeil",
"move-progress-pause": "Oedwch",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Math",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Galluogi",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Defnyddiwch GridFS MongoDB ar gyfer storio ffeiliau",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Wedi methu ag atal mudo",
"migration-stopped": "Daeth y mudo i ben yn llwyddiannus",
"mongodb-gridfs-storage": "Storio GridFS MongoDB",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Oedwch Pob Ymfudiad",
"s3-access-key": "Allwedd Mynediad S3",
"s3-access-key-description": "Allwedd mynediad AWS S3 ar gyfer dilysu",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Os cewch wallau wrth fewnforio bwrdd, weithiau mae mewnforio yn dal i weithio, ac mae bwrdd ar dudalen All Boards.",
"import-json-placeholder": "Gludwch eich data JSON dilys yma",
"import-csv-placeholder": "Gludwch eich data CSV/TSV dilys yma",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Aelodau map",
"import-members-map": "Mae gan eich bwrdd wedi'i fewnforio rai aelodau. Mapiwch yr aelodau yr ydych am eu mewnforio i'ch defnyddwyr",
"import-members-map-note": "Nodyn: Bydd aelodau heb eu mapio yn cael eu neilltuo i'r defnyddiwr presennol.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Mae trefn ymlaen",
"cardsSortPopup-title": "Cardiau Didoli",
"due-date": "Dyddiad Cwblhau",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Gwall Gweinydd",
"server-error-troubleshooting": "Cyflwynwch y gwall a gynhyrchwyd gan y gweinydd.\nAr gyfer gosodiad snap, rhedwch: `sudo snap logs wekan.wekan`\nAr gyfer gosodiad Docker, rhedwch: `sudo docker logs wekan-app`",
"title-alphabetically": "Teitl (Yn nhrefn yr wyddor)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Ymlyniadau",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Ffeil",
"move-progress-pause": "Oedwch",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Math",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS Galluogi",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Defnyddiwch GridFS MongoDB ar gyfer storio ffeiliau",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Wedi methu ag atal mudo",
"migration-stopped": "Daeth y mudo i ben yn llwyddiannus",
"mongodb-gridfs-storage": "Storio GridFS MongoDB",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Oedwch Pob Ymfudiad",
"s3-access-key": "Allwedd Mynediad S3",
"s3-access-key-description": "Allwedd mynediad AWS S3 ar gyfer dilysu",
+380 -243
View File
@@ -1,6 +1,6 @@
{
"accept": "Acceptér",
"act-activity-notify": "Aktivitetsnotits",
"accept": "Accepter",
"act-activity-notify": "Aktivitetsnotifikation",
"act-addAttachment": "tilføjede vedhæftningen __attachment__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-deleteAttachment": "slettede vedhæftning __attachment__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-addSubtask": "tilføjede delopgaven __subtask__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
@@ -9,12 +9,12 @@
"act-removeLabel": "Fjernede etiketten __label__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på kortet __board__",
"act-removedLabel": "Fjernede etiketten __label__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på kortet __board__",
"act-addChecklist": "tilføjede tjeklisten __checklist__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-addChecklistItem": "tilføjede elementet i tjekliste __checklistItem__ til tjeklisten __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-addChecklistItem": "tilføjede elementet i tjeklisten __checklistItem__ til tjeklisten __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-removeChecklist": "fjernede tjeklisten __checklist__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-removeChecklistItem": "fjernede elementet i tjekliste __checklistItem__ fra tjeklisten __checkList__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-removeChecklistItem": "fjernede elementet i tjeklisten __checklistItem__ fra tjeklisten __checkList__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-checkedItem": "markerede __checklistItem__ fra tjeklisten __checklist__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-uncheckedItem": "afmarkerede __checklistItem__ fra tjeklisten __checklist__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-completeChecklist": "afsluttet tjekliste __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-completeChecklist": "tjekliste fuldført __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-uncompleteChecklist": "uafsluttet tjekliste __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-addComment": "kommenterede på kortet __card__: __comment__ til listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-editComment": "redigerede kommentar på kortet __card__: __comment__ til listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
@@ -26,11 +26,11 @@
"act-deleteCustomField": "slettede brugerdefineret felt __customField__ på tavlen __board__",
"act-setCustomField": "redigerede brugerdefineret felt __customField__: __customFieldValue__ i kortet __card__ på lsten __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-createList": "tilføjede listen __list__ til tavlen __board__",
"act-addBoardMember": "tilføejede medlemmet __member__ til tavlen __board__",
"act-addBoardMember": "tilføjede medlemmet __member__ til tavlen __board__",
"act-archivedBoard": "Tavlen __board__ blev flyttet til Arkiv",
"act-archivedCard": "Kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til arkivet",
"act-archivedList": "Listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til arkivet",
"act-archivedSwimlane": "Svømmebanen __swimlane__ på tavlen __board__ er flyttet til arkivet",
"act-archivedCard": "Kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til Arkiv",
"act-archivedList": "Listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til Arkiv",
"act-archivedSwimlane": "Svømmebanen __swimlane__ på tavlen __board__ er flyttet til Arkiv",
"act-importBoard": "importerede tavlen __board__",
"act-importCard": "importerede kortet __card__ til listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"act-importList": "importerede listen __list__ til svømmebanen __swimlane__ på tavlen __board__",
@@ -49,80 +49,80 @@
"activity-archived": "%s flyttet til Arkiv",
"activity-attached": "vedhæftede %s til %s",
"activity-created": "oprettede %s",
"activity-changedListTitle": "döpte om listan till %s",
"activity-changedListTitle": "omdøbte listen til %s",
"activity-customfield-created": "oprettede brugerdefineret felt %s",
"activity-excluded": "ekskluderet %s fra %s",
"activity-imported": "importerede %s ind i %s fra %s",
"activity-excluded": "ekskluderede %s fra %s",
"activity-imported": "importerede %s til %s fra %s",
"activity-imported-board": "importerede %s fra %s",
"activity-joined": "indgik i %s",
"activity-joined": "sammenlagt med %s",
"activity-moved": "flyttede %s fra %s til %s",
"activity-on": "per %s",
"activity-removed": "fjernede %s fra %s",
"activity-sent": "sendte %s til %s",
"activity-unjoined": "udgik fra %s",
"activity-unjoined": "adskilt fra %s",
"activity-subtask-added": "tilføjede delopgave til %s",
"activity-checked-item": "afkrydsede %s i tjeklisten %s af %s",
"activity-checked-item": "markerede %s i tjeklisten %s af %s",
"activity-unchecked-item": "fjernede kryds %s i tjeklisten %s af %s",
"activity-checklist-added": "tilføjede tjeklisten til %s",
"activity-checklist-removed": "fjernede en tjekliste fra %s",
"activity-checklist-completed": "færdiggjorde tjekliste %s af %s",
"activity-checklist-uncompleted": "gjorde tjeklisten ukomplet, %s af %s",
"activity-checklist-uncompleted": "ændrede tjekliste %s af %s til ikke-færdiggjort",
"activity-checklist-item-added": "tilføjede element i tjekliste til '%s' i %s",
"activity-checklist-item-removed": "fjernede element i tjekliste fra '%s' i %s",
"add": "Tilføj",
"activity-checked-item-card": "markerede %s i tjeklisten %s",
"activity-unchecked-item-card": "afmarkerede %s i tjeklisten %s",
"activity-checklist-completed-card": "afsluttet tjekliste __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
"activity-checklist-uncompleted-card": "gjorde tjeklisten ukomplet %s",
"activity-checklist-uncompleted-card": "ændrede tjekliste %s til ikke-færdiggjort",
"activity-editComment": "redigerede kommentar %s",
"activity-deleteComment": "slettede kommentar %s",
"activity-receivedDate": "redigerade mottaget datum till %s av %s",
"activity-startDate": "redigerade startdatum till %s av %s",
"allboards.starred": " Stjärnmärkt",
"activity-receivedDate": "redigerede modtaget datoen til %s af %s",
"activity-startDate": "redigerede startdatoen til %s af %s",
"allboards.starred": "Stjernemarkeret",
"allboards.templates": "Skabeloner",
"allboards.remaining": "Återstående",
"allboards.workspaces": "Arbetsytor",
"allboards.add-workspace": "Lägg till arbetsyta",
"allboards.add-workspace-prompt": "Arbetsytans namn",
"allboards.add-subworkspace": "Lägg till underarbetsyta",
"allboards.add-subworkspace-prompt": "Underarbetsytans namn",
"allboards.edit-workspace": "Redigera arbetsyta",
"allboards.edit-workspace-name": "Arbetsytans namn",
"allboards.edit-workspace-icon": "Arbetsytans ikon (markdown)",
"allboards.workspace-menu": "Arbetsytans meny",
"workspace-settings": "Arbetsytans inställningar",
"workspaceActionsPopup-title": "Arbetsytans inställningar",
"allboards.workspace-color": "Färg",
"allboards.delete-workspace-confirm": "Är du säker på att du vill ta bort den här arbetsytan?",
"allboards.delete-workspace-confirm-check": "Bekräfta borttagning av arbetsyta",
"multi-selection-active": "Klicka i kryssrutor för att välja tavlor",
"activity-dueDate": "redigerade förfallodag till %s av %s",
"activity-endDate": "redigerade slutdatum till %s av %s",
"allboards.remaining": "Udestående",
"allboards.workspaces": "Arbejdsområder",
"allboards.add-workspace": "Tilføj Arbejdsområde",
"allboards.add-workspace-prompt": "Arbejdsområdets navn",
"allboards.add-subworkspace": "Tilføj Underarbejdsområde",
"allboards.add-subworkspace-prompt": "Underarbejdsområdets navn",
"allboards.edit-workspace": "Rediger arbejdsområde",
"allboards.edit-workspace-name": "Arbejdsområdets navn",
"allboards.edit-workspace-icon": "Arbejdsområdets ikon (markdown)",
"allboards.workspace-menu": "Arbejdsområdets menu",
"workspace-settings": "Arbejdsområdets indstillinger",
"workspaceActionsPopup-title": "Arbejdsområdets indstillinger",
"allboards.workspace-color": "Farve",
"allboards.delete-workspace-confirm": "Er du sikker på, at du vil slette dette arbejdsområde?",
"allboards.delete-workspace-confirm-check": "Bekræft sletning af arbejdsområdet",
"multi-selection-active": "Klik på tjekboksene for at vælge tavler",
"activity-dueDate": "Redigerede forfaldsdato til %s af %s",
"activity-endDate": "Redigerede slutdato til %s af %s",
"add-attachment": "Tilføj vedhæftning",
"add-board": "Tilføj tavle",
"add-template": "Lägg till mall",
"add-card": "Tilføj kort",
"add-card-to-top-of-list": "Lägg till kort överst i listan",
"add-card-to-bottom-of-list": "Lägg till kort i botten av listan",
"add-card-to-top-of-list": "Tilføj kort øverst i listen",
"add-card-to-bottom-of-list": "Tilføj kort nederst i listen",
"addListPopup-title": "Tilføj liste",
"setListWidthPopup-title": "Ställ in min bredd",
"set-list-width": "Ställ in min bredd",
"set-list-width-value": "Min och max bredd (pixlar)",
"list-width-error-message": "Listbredder måste vara heltal större än 100",
"keyboard-shortcuts-enabled": "Tangentbordsgenvägar aktiverade. Klicka för att inaktivera.",
"keyboard-shortcuts-disabled": "Tangentbordsgenvägar inaktiverade. Klicka för att aktivera.",
"setListWidthPopup-title": "Indstil bredder",
"set-list-width": "Indstil bredder",
"set-list-width-value": "Indstil min. og maks. bredder (pixels)",
"list-width-error-message": "Listebredder skal være heltal større end 100",
"keyboard-shortcuts-enabled": "Tastaturgenveje aktiverede. Klik for at deaktivere.",
"keyboard-shortcuts-disabled": "Tastaturgenveje deaktiverede. Klik for at aktivere.",
"setSwimlaneHeightPopup-title": "Angiv højde på svømmebane",
"set-swimlane-height": "Angiv højde på svømmebane",
"set-swimlane-height-value": "Svømmebanes bredde (pixels)",
"swimlane-height-error-message": "Svømmebreddes højde skal være et positivt heltal",
"swimlane-height-error-message": "Svømmebanens højde skal være et positivt heltal",
"add-swimlane": "Tilføj svømmebane",
"add-subtask": "Tilføj delopgave",
"add-checklist": "Tilføj tjekliste",
"checklist": "Checklista",
"checklist": "Tjekliste",
"add-checklist-item": "Tilføj et element til tjeklisten",
"close-add-checklist-item": "Stäng lägg till en rad i checklistans formulär",
"close-edit-checklist-item": "Stäng ändra en rad i checklistans formulär",
"convertChecklistItemToCardPopup-title": "Ändra till kort",
"close-add-checklist-item": "Luk tilføj et element til tjeklisteformular",
"close-edit-checklist-item": "Luk ændre et element til tjeklisteformular",
"convertChecklistItemToCardPopup-title": "Ændre til kort",
"add-cover": "Tilføj coverbillede til minikort",
"add-label": "Tilføj etikette",
"add-list": "Tilføj liste",
@@ -131,8 +131,8 @@
"added": "Tilføjet",
"addMemberPopup-title": "Tilføj medlemmer",
"memberPopup-title": "Medlemsindstillinger",
"admin": "Adminstratör",
"admin-desc": "Kan visa och redigera kort, ta bort medlemmar och ändra inställningar för tavlan. Kan visa aktiviteter.",
"admin": "Administrator",
"admin-desc": "Kan vise of redigere kort, fjerne medlemmer og ændre indstillinger for tavlen. Kan vise aktiviteter.",
"admin-announcement": "Annoncering",
"admin-announcement-active": "Aktivér annoncering på tværs af systemet",
"admin-announcement-title": "Annoncering fra administrator",
@@ -140,12 +140,12 @@
"and-n-other-card": "Samt __count__ andre kort",
"and-n-other-card_plural": "Samt __count__ andre kort",
"apply": "Anvend",
"app-is-offline": "Indlæser, vent venligst. Genopfriskes siden er der risiko for tab af data. Fungerer indlæsningen ikke, så tjek venligst om serveren er stoppet.",
"app-try-reconnect": "Försök att återansluta",
"app-is-offline": "Indlæser, vent venligst. Hvis siden genopfriskes, er der risiko for tab af data. Fungerer indlæsningen ikke, så tjek venligst om serveren er stoppet.",
"app-try-reconnect": "Prøv at genoprette forbindelsen.",
"archive": "Flyt til arkiv",
"archive-all": "Flyt alle til arkiv",
"archive-board": "Flyt tavle til arkiv",
"archive-board-confirm": " Är du säker på att du vill arkivera den här tavlan?",
"archive-board-confirm": "Er du sikker på, at du vil arkivere denne tavle?",
"archive-card": "Flyt kort til arkiv",
"archive-list": "Flyt liste til arkiv",
"archive-swimlane": "Flyt svømmebane til arkiv",
@@ -158,12 +158,12 @@
"archives": "Arkiv",
"template": "Skabelon",
"templates": "Skabeloner",
"template-container": "Mallbibliotek",
"add-template-container": "Lägg till från mall",
"assign-member": "Tilknyt medlem",
"template-container": "Skabelonsamling",
"add-template-container": "Tilføj skabelonsamling",
"assign-member": "Tildel medlem",
"attached": "vedhæftet",
"attachment": "Vedhæftning",
"attachment-delete-pop": "Slettes en vedhæftning sker det permanent. Det kan ikke omgøres.",
"attachment-delete-pop": "Slettes en vedhæftning, sker det permanent. Det kan ikke fortrydes efterfølgende.",
"attachmentDeletePopup-title": "Slet vedhæftning?",
"attachments": "Vedhæftninger",
"auto-watch": "Følg automatisk tavler når de oprettes",
@@ -171,25 +171,25 @@
"back": "Tilbage",
"board-change-color": "Skift farve",
"board-change-background-image": "Skift baggrundsbillede",
"board-background-image-url": "Bakgrundsbildens URL",
"add-background-image": "Lägg till bakgrundsbild",
"remove-background-image": "Ta bort bakgrundsbild",
"show-at-all-boards-page": "Visa på sidan alla tavlor",
"board-info-on-my-boards": "Alla tavlors inställningar",
"boardInfoOnMyBoardsPopup-title": "Alla tavlors inställningar",
"boardInfoOnMyBoards-title": "Alla tavlors inställningar",
"show-card-counter-per-list": "Visa antal kort per lista",
"show-board_members-avatar": "Visa medlemmarnas avatarer",
"board_members": "Alla tavlans medlemmar",
"card_members": "Alla medlemmar i detta kort på tavlan",
"board_assignees": " Alla tilldelade i alla kort på tavlan",
"card_assignees": "Alla tilldelade i detta kort på tavlan",
"board-background-image-url": "Baggrundsbilledets URL",
"add-background-image": "Tilføj baggrundsbillede",
"remove-background-image": "Fjern baggrundsbillede",
"show-at-all-boards-page": "Vis på siden Alle Tavler",
"board-info-on-my-boards": "Indstillinger for Alle Tavler",
"boardInfoOnMyBoardsPopup-title": "Indstillinger for Alle Tavler",
"boardInfoOnMyBoards-title": "Indstillinger for Alle Tavler",
"show-card-counter-per-list": "Vis antal kort per liste",
"show-board_members-avatar": "Vis avatarer for medlemmer",
"board_members": "Alle tavlens medlemmer",
"card_members": "Alle medlemmer for dette kort på tavlen",
"board_assignees": "Alle tildelte medlemmer på alle kort på tavlen",
"card_assignees": "Alle tildelte medlemmer på dette kort på tavlen",
"board-nb-stars": "%s stjerner",
"board-not-found": "Fandt ikke tavle",
"board-not-found": "Tavlen blev ikke fundet",
"board-private-info": "Denne tavle vil være <strong>privat</strong>.",
"board-public-info": "Denne tavle vil være <strong>offentlig</strong>.",
"board-drag-drop-reorder-or-click-open": "Dra och släpp för att ändra ordning på tavelsymboler. Klicka på tavelsymbolen för att öppna tavlan.",
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
"board-drag-drop-reorder-or-click-open": "Træk og slip for at arrangere tavleikoner. Klik på tavleikon for at åbne tavle.",
"board-open-and-move-between-remaining-and-workspaces": "Klik for at åbne tavle. Træk-og-slip for at tildele tavle til __workspaces__ (slip på Arbejdsområde i sidemenuen).",
"boardChangeColorPopup-title": "Skift tavlens baggrund",
"boardChangeBackgroundImagePopup-title": "Skift baggrundsbillede",
"allBoardsChangeColorPopup-title": "Skift farve",
@@ -202,37 +202,37 @@
"boardChangeViewPopup-title": "Tavlevisning",
"boards": "Tavler",
"board-view": "Tavlevisning",
"desktop-mode": " Skrivbordsläge",
"mobile-mode": "Mobilläge",
"mobile-desktop-toggle": " Växla mellan mobilläge och skrivbordsläge",
"zoom-in": "Zooma in",
"zoom-out": "Zooma ut",
"click-to-change-zoom": " Klicka för att ändra zoomnivå",
"zoom-level": "Zoomnivå",
"enter-zoom-level": "Ange zoomnivå (50-300%):",
"desktop-mode": "Skrivebordstilstand",
"mobile-mode": "Mobiltilstand",
"mobile-desktop-toggle": "Skift imellem mobil- og skrivebordstilstand",
"zoom-in": "Zoom ind",
"zoom-out": "Zoom ud",
"click-to-change-zoom": "Klik for at ændre zoom niveau",
"zoom-level": "Zoom niveau",
"enter-zoom-level": "Angiv zoom niveau (50-300%):",
"board-view-cal": "Kalender",
"board-view-swimlanes": "Svømmebaner",
"board-view-collapse": "Sammenfold",
"board-view-gantt": "Gantt",
"board-view-lists": "Lister",
"bucket-example": " Som till exempel \"Bucket List\"",
"calendar-previous-month-label": " Föregående månad",
"calendar-next-month-label": " Nästa månad",
"cancel": "Annullér",
"card-archived": "Dette kort blev flyttet til arkivet.",
"board-archived": "Denne tavle blev flyttet til arkivet.",
"bucket-example": "F.eks. \"Bucket List\"",
"calendar-previous-month-label": "Forgående måned",
"calendar-next-month-label": "Næste måned",
"cancel": "Annuller",
"card-archived": "Dette kort blev flyttet til Arkiv.",
"board-archived": "Denne tavle blev flyttet til Arkiv.",
"card-comments-title": "Dette kort har %s kommentar.",
"card-delete-notice": "Sletning vil være permanent. Du mister alle handlinger knyttet til dette kort.",
"card-delete-pop": "Alle handlinger vil blive fjernet fra aktivitetsfeedet, og du kan ikke genåbne kortet. Det kan ikke omgøres.",
"card-delete-suggest-archive": "Du kan flytte et kort til arkivet for at fjerne det fra tavlen, og bevare aktiviteten.",
"card-archive-pop": "Kortet kommer inte synas i denna listan efter det är arkiverat.",
"card-archive-suggest-cancel": "Du kan återställa kort från Arkivet",
"list-archive-pop": "Listan kommer inte att synas på tavlan efter arkivering.",
"list-archive-suggest": "Du kan senare återställa listan från Arkivet i Tavelinställningar.",
"listArchivePopup-title": "Arkivera lista?",
"swimlane-archive-pop": "Simbana kommer inte att synas på tavlan efter arkivering.",
"swimlane-archive-suggest": "Du kan senare återställa simbanan från Arkivet i Tavelinställningar.",
"swimlaneArchivePopup-title": "Arkivera simbana?",
"card-delete-pop": "Alle handlinger vil blive fjernet fra aktivitetsfeedet, og du kan ikke genåbne kortet. Det kan ikke fortrydes efterfølgende.",
"card-delete-suggest-archive": "Du kan flytte et kort til arkivet for at fjerne det fra tavlen og bevare aktiviteten.",
"card-archive-pop": "Kortet vil ikke være synligt på denne liste efter arkivering.",
"card-archive-suggest-cancel": "Du kan gendanne kortet senere fra Arkiv.",
"list-archive-pop": "Listen vil ikke være synligt på denne liste efter arkivering.",
"list-archive-suggest": "Du kan gendanne listen senere fra Tavleindstillinger.",
"listArchivePopup-title": "Arkiver liste?",
"swimlane-archive-pop": "Svømmebane vil ikke være synligt på denne liste efter arkivering.",
"swimlane-archive-suggest": "Du kan gendanne svømmebanen senere fra Tavleindstillinger.",
"swimlaneArchivePopup-title": "Arkiver svømmebane?",
"card-due": "Forfalder",
"card-due-on": "Forfaldsdato",
"card-spent": "Anvendt tid",
@@ -240,10 +240,10 @@
"card-edit-custom-fields": "Redigér brugerdefinerede felter",
"card-edit-labels": "Redigér etiketter",
"card-edit-members": "Redigér medlemmer",
"card-labels-title": "Ændr etiketter for kortet.",
"card-labels-title": "Ændre etiketter for kortet.",
"card-members-title": "Tilføj eller fjern medlemmer på tavlen fra kortet.",
"card-start": "Påbörjades",
"card-start-on": "Starter per",
"card-start": "Påbegynd",
"card-start-on": "Påbegyndes per",
"cardAttachmentsPopup-title": "Vedhæft fra",
"cardCustomField-datePopup-title": "Ændringsdato",
"cardCustomFieldsPopup-title": "Redigér brugerdefinerede felter",
@@ -259,9 +259,9 @@
"vote-against": "går imod",
"deleteVotePopup-title": "Slet afstemning?",
"vote-delete-pop": "Sletning er permanent. Du mister alle handlinger knyttet til denne afstemning.",
"cardStartPlanningPokerPopup-title": "Starta en planeringspoker.",
"card-edit-planning-poker": "Ändra planeringspoker",
"editPokerEndDatePopup-title": "Ändra planeringspokerns sista röstdag",
"cardStartPlanningPokerPopup-title": "Opret en rangering",
"card-edit-planning-poker": "Ændre rangering",
"editPokerEndDatePopup-title": "Ændre rangeringsafstemningens slutdato",
"poker-question": "Rangering",
"poker-one": "1",
"poker-two": "2",
@@ -273,15 +273,15 @@
"poker-forty": "40",
"poker-oneHundred": "100",
"poker-unsure": "?",
"poker-finish": "Klar",
"poker-result-votes": "Röster",
"poker-result-who": "Vem",
"poker-replay": "Upprepa",
"set-estimation": "Ange uppskattning",
"deletePokerPopup-title": "Radera planeringspoker?",
"poker-delete-pop": "Raderingen är permanent. Du vill förlora alla associationer med planneringspokern.",
"poker-finish": "Afslut",
"poker-result-votes": "Stemmer",
"poker-result-who": "Hvem",
"poker-replay": "Gentag",
"set-estimation": "Angiv estimering",
"deletePokerPopup-title": "Vil du slette rangeringen?",
"poker-delete-pop": "Sletningen er permanent. Du vil miste alle handlinger associeret med denne rangering.",
"cardDeletePopup-title": "Slet kort?",
"cardArchivePopup-title": "Arkivera kort?",
"cardArchivePopup-title": "Arkiver kort?",
"cardDetailsActionsPopup-title": "Handlinger for kort",
"cardLabelsPopup-title": "Etiketter",
"cardMembersPopup-title": "Medlemmer",
@@ -300,8 +300,8 @@
"change-permissions": "Tilpas tilladelser",
"change-settings": "Tilpas indstillinger",
"changeAvatarPopup-title": "Tilpas avatar",
"delete-avatar-confirm": "Är du säker på att du vill ta bort denna avatar?",
"deleteAvatarPopup-title": "Ta bort avatar?",
"delete-avatar-confirm": "Er du sikker på, at du vil slette denne avatar?",
"deleteAvatarPopup-title": "Slet avatar?",
"changeLanguagePopup-title": "Skift sprog",
"changePasswordPopup-title": "Skift kodeord",
"changePermissionsPopup-title": "Tilpas tilladelser",
@@ -310,17 +310,17 @@
"checklists": "Tjeklister",
"click-to-star": "Klik for at tilføje stjerne til tavlen.",
"click-to-unstar": "Klik for at fjerne stjerne fra tavlen.",
"click-to-enable-auto-width": "Automatisk listbredd inaktiverad. Klicka för att aktivera.",
"click-to-disable-auto-width": "Automatisk listbredd aktiverad. Klicka för att inaktivera.",
"auto-list-width": "Automatisk listbredd",
"click-to-enable-auto-width": "Automatisk listebredde deaktiveret. Klik for at aktivere.",
"click-to-disable-auto-width": "Automatisk listebredde aktiveret. Klik for at deaktivere.",
"auto-list-width": "Automatisk listebredde",
"clipboard": "Udklipsholder eller træk-og-slip",
"close": "Luk",
"close-board": "Luk tavle",
"close-board-pop": "Du har mulighed for at genskabe tavlen ved at klikke på \"Arkiv\"-knappen fra overskriften hjem.",
"close-card": "Stäng kortet",
"close-card": "Luk kortet",
"color-black": "sort",
"color-blue": "blå",
"color-crimson": "crimsonrød",
"color-crimson": "karmosinrød",
"color-darkgreen": "mørkegrøn",
"color-gold": "guld",
"color-gray": "grå",
@@ -349,38 +349,38 @@
"comment-placeholder": "",
"comment-only": "Kun kommentarer",
"comment-only-desc": "Kan kun kommentere på kort.",
"comment-assigned-only": "Endast tilldelad kommentar",
"comment-assigned-only-desc": "Endast tilldelade kort synliga. Kan endast kommentera.",
"comment-delete": "Är du säker på att du vill radera kommentaren?",
"deleteCommentPopup-title": "Radera kommentaren?",
"comment-assigned-only": "Kun tildelt kommentarer.",
"comment-assigned-only-desc": "Kun tildelte kort er synlige. Kan kun kommentere.",
"comment-delete": "Er du sikker på, at du vil slette denne kommentar?",
"deleteCommentPopup-title": "Slet kommentaren?",
"no-comments": "Ingen kommentarer",
"no-comments-desc": "Kan inte se kommentarer.",
"read-only": "Skrivskyddad",
"read-only-desc": "Kan endast visa kort. Kan inte redigera.",
"read-assigned-only": "Endast tilldelad läsning",
"read-assigned-only-desc": "Endast tilldelade kort synliga. Kan inte redigera.",
"no-comments-desc": "Kan ikke se kommentarer.",
"read-only": "Skrivebeskyttet",
"read-only-desc": "Kan kun se kort, ikke redigere dem.",
"read-assigned-only": "Kun tildelt læsning",
"read-assigned-only-desc": "Kun tildelte kort er synlige. Kan ikke redigere.",
"worker": "Arbejder",
"worker-desc": "Kan kun flytte kort, tildele sig selv til kort og kommentere.",
"computer": "Dator",
"confirm-subtask-delete-popup": "Er du sikker på du vil slette delopgaven?",
"confirm-checklist-delete-popup": "Är du säker på att du vill radera checklistan?",
"confirm-checklist-item-delete-popup": "Är du säker på att du vill ta bort checklistepunkten?",
"subtaskDeletePopup-title": "Radera deluppgift?",
"checklistDeletePopup-title": "Radera checklistan?",
"checklistItemDeletePopup-title": "Radera checklistpost?",
"confirm-checklist-delete-popup": "Er du sikker på, at du vil slette tjeklisten?",
"confirm-checklist-item-delete-popup": "Er du sikker på, at du vil slette dette element på tjeklisten?",
"subtaskDeletePopup-title": "Slet delopgave?",
"checklistDeletePopup-title": "Slet tjeklisten?",
"checklistItemDeletePopup-title": "Slet element på tjeklisten?",
"copy-card-link-to-clipboard": "Kopiér link til kort til udklipsholder",
"copy-text-to-clipboard": "Kopiera text till urklipp",
"copy-text-to-clipboard": "Kopier tekst til udklipsholder",
"linkCardPopup-title": "Sammenkæd kort",
"searchElementPopup-title": "Søg",
"copyCardPopup-title": "Kopiér kort",
"copyListPopup-title": "Kopiera lista",
"copyManyCardsPopup-title": "Kopiera mall till många kort",
"copyCardPopup-title": "Kopier kort",
"copyListPopup-title": "Kopier liste",
"copyManyCardsPopup-title": "Kopier skabelon til mange kort",
"copyManyCardsPopup-instructions": "Destination for kortenes titler og beskrivelser i dette JSON-format",
"copyManyCardsPopup-format": "[ {\"title\": \"Första kortets titel\", \"description\":\"Första kortets beskrivning\"}, {\"title\":\"Andra kortets titel\",\"description\":\"Andra kortets beskrivning\"},{\"title\":\"Sista kortets titel\",\"description\":\"Sista kortets beskrivning\"} ]",
"copyManyCardsPopup-format": "[ {\"title\": \"Titel på første kort\", \"description\":\"Beskrivelse af første kort\"}, {\"title\":\"Andet korts titel\",\"description\":\"Beskrivelse af andet kort\"},{\"title\":\"Sidste korts titel\",\"description\":\"Beskrivelse af sidste kort\"} ]",
"create": "Opret",
"createBoardPopup-title": "Opret tavle",
"createTemplateContainerPopup-title": "Lägg till från mall",
"chooseBoardSourcePopup-title": "Importér tavle",
"createTemplateContainerPopup-title": "Tilføj skabelonsamling",
"chooseBoardSourcePopup-title": "Importer tavle",
"createLabelPopup-title": "Opret etikette",
"createCustomField": "Opret felt",
"createCustomFieldPopup-title": "Opret felt",
@@ -399,7 +399,7 @@
"custom-field-text": "Tekst",
"custom-fields": "Brugerdefinerede felter",
"date": "Dato",
"date-format": "Datumformat",
"date-format": "Datoformat",
"date-format-yyyy-mm-dd": "ÅÅÅÅ-MM-DD",
"date-format-dd-mm-yyyy": "DD-MM-ÅÅÅÅ",
"date-format-mm-dd-yyyy": "MM-DD-ÅÅÅÅ",
@@ -422,13 +422,13 @@
"editCardStartDatePopup-title": "Skift startdato",
"editCardDueDatePopup-title": "Skift forfaldsdato",
"editCustomFieldPopup-title": "Redigér felt",
"addReactionPopup-title": "Lägg till reaktion",
"addReactionPopup-title": "Tilføj reaktion",
"editCardSpentTimePopup-title": "Tilpas forbrugt tid",
"editLabelPopup-title": "Skift etikette",
"editNotificationPopup-title": "Redigér notifikation",
"editProfilePopup-title": "Redigér profil",
"email": "E-mail",
"email-address": "E-postadress",
"email": "Email",
"email-address": "Email adresse",
"email-enrollAccount-subject": "Der er oprettet konto til dig på __siteName__",
"email-enrollAccount-text": "Hej __user__,\n\nFor at begynde at benytte tjenesten, så klik linket nedenfor.\n\n__url__\n\nTak.",
"email-fail": "Afsendelse af e-mail mislykkedes",
@@ -442,12 +442,12 @@
"email-sent": "E-mail er afsendt",
"email-verifyEmail-subject": "Verificér din e-mailadresse på your __siteName__",
"email-verifyEmail-text": "Hej __user__,\n\nFor at verificere din e-mail for kontoen, så klik på linket nedenfor.\n\n__url__\n\nTak.",
"enable-vertical-scrollbars": "Aktivera vertikala rullningslister",
"enable-vertical-scrollbars": "Aktiver vertikale rullepaneler",
"enable-wip-limit": "Slå WIP-begrænsning til",
"error-board-doesNotExist": "Denne tavle eksisterer ikke.",
"error-board-notAdmin": "Du skal være administrator for tavlen for at gøre dette",
"error-board-notAMember": "Du skal være medlem af denne tavle for at gøre dette",
"error-notAllowed": "Your board role is not allowed to do that",
"error-notAllowed": "Din tavle-rolle har ikke tilladelse til at udføre denne handling",
"error-json-malformed": "Din tekst er ikke gyldig JSON",
"error-json-schema": "Dine JSON-data indeholder ikke den rette information i det rette format",
"error-csv-schema": "Din CSV(Comma Separated Values)/TSV (Tab Separated Values) innehåller inte korrekt information i rätt format.",
@@ -455,77 +455,77 @@
"error-user-doesNotExist": "Brugeren findes ikke",
"error-user-notAllowSelf": "Du kan ikke invitere dig selv",
"error-user-notCreated": "Brugeren er ikke oprettet",
"error-username-taken": "Brugernavnet er optaget",
"error-orgname-taken": "Organisations namnet är upptaget.",
"error-teamname-taken": "Detta team namn är redan taget.",
"error-email-taken": "E-mailadressen er allerede optaget",
"export-board": "Eksportér tavle",
"export-board-json": "Eksportér tavle til JSON",
"export-board-csv": "Eksportér tavle til CSV",
"export-board-csv-comma": "Exportera till CSV (avgränsare: ,)",
"export-board-csv-semicolon": "Exportera till CSV (avgränsare: ;)",
"export-board-tsv": "Eksportér tavle til TSV",
"export-board-excel": "Exportera tavla till Excel",
"user-can-not-export-excel": "Användare kan inte exportera till Excel",
"export-board-html": "Eksportér tavle til HTML",
"export-card": "Exportera kort",
"export-card-pdf": "Exportera kort till PDF",
"export-card-excel": "Exportera kort till Excel",
"export-card-excel-fields": "Välj fält att inkludera i Excel-export:",
"error-username-taken": "Brugernavnet er allerede i brug",
"error-orgname-taken": "Organisationsnavnet er allerede i brug",
"error-teamname-taken": "Holdnavnet er allerede i brug",
"error-email-taken": "Email adressen er allerede i brug",
"export-board": "Eksporter tavle",
"export-board-json": "Eksporter tavle til JSON",
"export-board-csv": "Eksporter tavle til CSV",
"export-board-csv-comma": "Eksporter til CSV (skilletegn: ,)",
"export-board-csv-semicolon": "Eksporter til CSV (skilletegn: ;)",
"export-board-tsv": "Eksporter tavle til TSV",
"export-board-excel": "Eksporter tavle til Excel",
"user-can-not-export-excel": "Brugeren kan ikke eksportere til Excel",
"export-board-html": "Eksporter tavle til HTML",
"export-card": "Eksporter kort",
"export-card-pdf": "Eksporter kort til PDF",
"export-card-excel": "Eksporter kort til Excel",
"export-card-excel-fields": "Vælg felter at inkludere i Excel eksport",
"export-card-subtasks": "Delopgaver",
"export-card-attachments": "Vedhæftninger",
"export-card-field-people": "Personer (Skapare, Ägare, Medlemmar, Tilldelade)",
"export-card-field-board-info": "Tavelinformation (Tavla, Lista, Swimlane)",
"export-card-field-dates": "Datum (Skapad, Mottagen, Start, Förfaller, Slut)",
"export-card-attachment-filename": "Filnamn",
"export-card-attachment-size": "Storlek",
"export-card-attachment-type": "Skriv",
"export-card-attachment-uploaded-by": "Uppladdat av",
"export-card-attachment-uploaded-at": "Uppladdat den",
"export-card-attachment-image-previews": "Förhandsvisning",
"export-card-excel-no-disk-space": "Kan inte exportera till Excel: inte tillräckligt med ledigt diskutrymme",
"export-card-field-people": "Personer (Opretter, Ejer, Medlemmer, Tildelte)",
"export-card-field-board-info": "Tavleinformation (Tavle, Liste, Svømmebane)",
"export-card-field-dates": "Datoer (Oprettet, Modtaget, Start, Forfald, Slut)",
"export-card-attachment-filename": "Filnavn",
"export-card-attachment-size": "Størrelse",
"export-card-attachment-type": "Type",
"export-card-attachment-uploaded-by": "Uploadet af",
"export-card-attachment-uploaded-at": "Uploaded d.",
"export-card-attachment-image-previews": "Forhåndsvisning",
"export-card-excel-no-disk-space": "Kan ikke eksportere til Excel: Ikke nok ledig diskplads",
"export-card-excel-free": "ledigt",
"export-card-excel-needed": "krävs",
"user-can-not-export-card-to-pdf": "Användaren kan inte exportera kort till PDF",
"user-can-not-export-card-to-excel": "Användaren kan inte exportera kort till Excel",
"exportBoardPopup-title": "Eksportér tavle",
"exportCardPopup-title": "Exportera kort",
"sort": "Sortér",
"sorted": "Sorterad",
"remove-sort": "Återställ sortering",
"export-card-excel-needed": "påkrævet",
"user-can-not-export-card-to-pdf": "Brugeren kan ikke eksportere kort til PDF",
"user-can-not-export-card-to-excel": "Brugeren kan ikke eksportere kort til Excel",
"exportBoardPopup-title": "Eksporter tavle",
"exportCardPopup-title": "Eksporter kort",
"sort": "Sorter",
"sorted": "Sorteret",
"remove-sort": "Fjern sortering",
"sort-desc": "Klik for at sortere listen",
"list-sort-by": "Sortér listen efter:",
"list-sort-by": "Sorter listen efter:",
"list-label-modifiedAt": "Senest tilgået:",
"list-label-title": "Navn på listen",
"list-label-sort": "Din manuelle ordre",
"list-label-short-modifiedAt": "(S)",
"list-label-short-title": "(N)",
"list-label-short-sort": "(M)",
"filter": "Filtrera",
"filter-cards": "Filtrér kort eller lister",
"filter-dates-label": "Filtrera efter datum",
"filter-no-due-date": "Inget förfallodatum",
"filter-overdue": "Försenad",
"filter-due-today": "Förfaller idag",
"filter-due-this-week": "Förfaller denna vecka",
"filter-due-next-week": "Förfaller denna vecka",
"filter-due-tomorrow": "Förfaller imorgon",
"list-filter-label": "Filtrér lister efter titel",
"filter-clear": "Ryd filter",
"filter-labels-label": "Filtrera på etikett",
"filter": "Filter",
"filter-cards": "Filtrer kort eller lister",
"filter-dates-label": "Filtrer efter dato",
"filter-no-due-date": "Ingen forfaldsdato",
"filter-overdue": "Forfaldsdato overskredet",
"filter-due-today": "Forfalder i dag",
"filter-due-this-week": "Forfalder i denne uge",
"filter-due-next-week": "Forfalder i næste uge",
"filter-due-tomorrow": "Forfalder i morgen",
"list-filter-label": "Filtrer liste efter titel",
"filter-clear": "Ryd filtrering",
"filter-labels-label": "Filtrer på etikette",
"filter-no-label": "Ingen etikette",
"filter-member-label": "Filtrera på medlem",
"filter-member-label": "Filtrer på medlem",
"filter-no-member": "Ingen medlemmer",
"filter-assignee-label": "Filtrera på tilldelad till",
"filter-assignee-label": "Filtrer på tildelt til",
"filter-no-assignee": "Utildelt",
"filter-custom-fields-label": "Filtrera på anpassade fält",
"filter-custom-fields-label": "Filtrer på brugerdefineret felt",
"filter-no-custom-fields": "Ingen brugerdefinerede felter",
"filter-show-archive": "Vis arkiverede lister",
"filter-hide-empty": "Skjul tomme lister",
"filter-on": "Filter er slået til",
"filter-on-desc": "Du filtrerer kort på denne tavle. Klik her for at redigere filteret.",
"filter-to-selection": "Filtrér til valgte",
"other-filters-label": "Andra filter",
"filter-to-selection": "Filtrer til valgte",
"other-filters-label": "Andre filtre",
"advanced-filter-label": "Avanceret filter",
"advanced-filter-description": "Avanceret filter gør det muligt at skrive en tekststreng indeholdende følgende operatører: == != <= >= && || ( ) Mellemrum anvendes som adskillelsestegn mellem operatørerne. Du kan filtrere alle Brugerdefinerede felter ved at taste deres navne og værdier. Som eksempel: Felt1 == Værdi1. Bemærk: Hvis felter eller værdier indeholder mellemrum, så skal du indkapsle dem i enkeltcitationstegn. Som eksempel: 'Felt 1' == 'Værdi1'. For at springe over enkelte kontroltegn (' \\\\/), så kan \\\\ benyttes. Som eksempel: Felt1 == Så\\\\'n. Du kan også kombinere flere betingelser. Som eksempel: F1 == V1 || F1 == V2. Normalt vil alle operatører blive fortolket fra venstre mod højre. Du kan ændre rækkefølgen ved brug af parenteser. Som eksempel: F1 == V1 && (F2 == V2 || F2 == V3). Du kan også søge i tekstfelter med brug af regulære udtryk: F1 == /Tes.*/i",
"fullname": "Fuldt navn",
@@ -534,13 +534,13 @@
"headerBarCreateBoardPopup-title": "Opret tavle",
"home": "Hjem",
"import": "Importér",
"impersonate-user": "Imitera användare",
"link": "Länk",
"import-board": "importér tavle",
"import-board-c": "Importér tavle",
"import-board-title-trello": "Importér tavle fra Trello",
"import-board-title-wekan": "Importér tavler fra tidligere eksport",
"import-board-title-csv": "Importér tavle fra CSV/TSV",
"impersonate-user": "Log ind som bruger",
"link": "Link",
"import-board": "importer tavle",
"import-board-c": "Importer tavle",
"import-board-title-trello": "Importer tavle fra Trello",
"import-board-title-wekan": "Importer tavler fra tidligere eksport",
"import-board-title-csv": "Importer tavle fra CSV/TSV",
"from-trello": "Fra Trello",
"from-wekan": "Fra forrige eksport",
"from-csv": "Fra CSV/TSV",
@@ -549,21 +549,64 @@
"import-board-instruction-wekan": "På din tavle, gå til 'Menu', dernæst 'Eksportér tavle', og kopiér teksten i den hentede fil.",
"import-board-instruction-about-errors": "Hvis du får fejl når der importeres en tavle, så vil importen undertiden stadig fungere, og tavlen vil være under side Alle tavler.",
"import-json-placeholder": "Indsæt dine gyldige JSON-data her",
"import-csv-placeholder": "Indsæt din valide CSV/TSV-data her",
"import-map-members": "Kortlæg medlemmer",
"import-csv-placeholder": "Indsæt dine gyldige CSV/TSV-data her",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Map medlemmer",
"import-members-map": "Dine importerede tavler rummer medlemmer. Kortlæg venligst de medlemmer du ønsker at importere til dine brugere.",
"import-members-map-note": "Notera: Ommappade medlemmar kommer tilldelas den nuvarande användaren.",
"import-show-user-mapping": "Gennemse kortlægning af medlemmer",
"import-members-map-note": "Bemærk: Ikke-mappede medlemmer vil blive mappet til den nuværende bruger.",
"import-show-user-mapping": "Gennemse mapning af medlemmer",
"import-user-select": "Vælg din eksisterende bruger, som du ønsker at bruge for dette medlem",
"importMapMembersAddPopup-title": "Vælg medlem",
"info": "Versjon",
"info": "Version",
"initials": "Initialer",
"invalid-date": "Ugyldig dato",
"invalid-time": "Ugyldig tidsangivelse",
"invalid-user": "Ugyldig bruger",
"joined": "sluttede sig til",
"joined": "sammenlagt",
"just-invited": "Du er netop blevet inviteret til denne tavle",
"keyboard-shortcuts": "Tastaturgenveje",
"keyboard-shortcuts": "Genvejstaster",
"label-create": "Opret etikette",
"label-default": "%s etikette (standard)",
"label-delete-pop": "Det er ikke muligt at fortryde. Dette vil fjerne etiketten fra alle kort og ødelægge dets historik.",
@@ -580,9 +623,9 @@
"list-select-cards": "Vælg alle kort i denne liste",
"set-color-list": "Angiv farve",
"listActionPopup-title": "Handlinger for liste",
"settingsUserPopup-title": "Användarinställningar",
"settingsTeamPopup-title": "Team inställningar",
"settingsOrgPopup-title": "Organisationens inställningar",
"settingsUserPopup-title": "Brugerindstillinger",
"settingsTeamPopup-title": "Holdindstillinger",
"settingsOrgPopup-title": "Organisationsindstillinger",
"swimlaneActionPopup-title": "Handlinger for svømmebane",
"swimlaneAddPopup-title": "Tilføj en svømmebane nedenfor",
"listImportCardPopup-title": "Importér et Trello-kort",
@@ -601,18 +644,18 @@
"memberMenuPopup-title": "Medlemsindstillinger",
"grey-icons": "Grå ikoner",
"members": "Medlemmer",
"menu": "Meny",
"menu": "Menu",
"move-selection": "Flyt valgte",
"moveCardPopup-title": "Flyt kort",
"moveListPopup-title": "Flytta lista",
"moveCardToBottom-title": "Flyt til bunden",
"moveCardToTop-title": "Flyt til toppen",
"moveSelectionPopup-title": "Flyt valgte",
"copySelectionPopup-title": "Kopiera markering",
"selection-color": "Markeringsfärg",
"copySelectionPopup-title": "Kopier markering",
"selection-color": "Markeringsfarve",
"multi-selection": "Multivalg",
"multi-selection-label": "Ange etikett för val",
"multi-selection-member": "Ange medlem för val",
"multi-selection-label": "Angiv etikette for valg",
"multi-selection-member": "Angiv medlem for valg",
"multi-selection-on": "Multivalg er slået til",
"muted": "Slukket",
"muted-info": "Du vil aldrig få notifikationer om ændringer i denne tavle",
@@ -624,10 +667,10 @@
"no-results": "Ingen resultater",
"normal": "Normal",
"normal-desc": "Du kan se og redigere kort. Indstillinger kan ikke ændres.",
"normal-assigned-only": "Endast tilldelad normal",
"normal-assigned-only-desc": " Endast tilldelade kort synliga. Redigera som normal användare.",
"normal-assigned-only": "Kun tildelt normal",
"normal-assigned-only-desc": "Kun tildelte kort synlige. Rediger som normal bruger.",
"not-accepted-yet": "Invitation er endnu ikke accepteret",
"notify-participate": "Få uppdateringar på alla kort du är med som deltagare, skapare eller medlem",
"notify-participate": "Modtag opdateringer på alle kort, som du deltager i som opretter eller medlem",
"notify-watch": "Modtag opdateringer for alle tavler eller kort som du følger",
"optional": "valgfri",
"or": "eller",
@@ -645,29 +688,29 @@
"public": "Offentlig",
"public-desc": "Denne tavle er offentlig. Den er synlig for alle med linket og vil blive vist i søgemaskiner som Google. Det er kun personer tilføjet til tavlen, der kan redigere.",
"quick-access-description": "Stjernemarkér en tavle for at tilføje genvej i denne bjælke.",
"remove-cover": "Ta bort omslagsbild för minikort",
"remove-cover": "Fjern omslagsbillede for minikort",
"remove-from-board": "Fjern fra tavle",
"remove-label": "Fjern etikette",
"listDeletePopup-title": "Slet liste?",
"remove-member": "Fjern medlem",
"remove-member-from-card": "Fjern fra kort",
"remove-member-pop": "Fjern __name__ (__username__) fra __boardTitle__? Medlemmet vil blive fjernetfra alle kort på denne tavle. De vil modtage en notifikation.",
"remove-member-pop": "Fjern __name__ (__username__) fra __boardTitle__? Medlemmet vil blive fjernet fra alle kort på denne tavle. De vil modtage en notifikation.",
"removeMemberPopup-title": "Fjern medlem?",
"rename": "Omdøb",
"rename-board": "Omdøb tavle",
"restore": "Genskab",
"rescue-card-description": "Visa räddningsdialogruta innan stängning för osparade kortbeskrivningar",
"rescue-card-description-dialogue": "Skriv över kortbeskrivningen med dina ändringar?",
"rescue-card-description": "Vis redningsdialog inden lukning af ikke-gemte kort beskrivelser ",
"rescue-card-description-dialogue": "Overskriv nuværende kort beskrivelse med dine ændringer?",
"save": "Gem",
"search": "Søg",
"rules": "Regler",
"search-cards": "Søg ud fra titler i kort/lister, beskrivelser og brugerdefinerede felter på denne tavle.",
"search-example": "Text att söka efter?",
"search-example": "Skriv den tekst, som du søger efter og tryk på Enter",
"select-color": "Vælg farve",
"select-board": "Välj tavla",
"select-board": "Vælg tavle",
"set-wip-limit-value": "Angiv en grænse for det maksimale antal opgaver i denne liste",
"setWipLimitPopup-title": "Angiv WIP-begrænsning",
"shortcut-add-self": "Tilldela dig nuvarande kort",
"shortcut-add-self": "Tildel dig selv til nuværende kort",
"shortcut-assign-self": "Tilknyt dig selv til nuværende kort?",
"shortcut-autocomplete-emoji": "Auto-fuldfør emoji",
"shortcut-autocomplete-members": "Auto-fuldfør medlemmer",
@@ -697,23 +740,23 @@
"has-spenttime-cards": "Har kort med anvendt tid",
"time": "Tid",
"title": "Titel",
"toggle-assignees": "Växla tilldelade personer 1-9 för kort (I den ordning de lades till i tavlan).",
"toggle-labels": "Toggle-etiketter 1-9 för kortet. Multi-Selection lägger till etiketter 1-9.",
"remove-labels-multiselect": "Multi-Selection raderar etikett 1-9",
"toggle-assignees": "Skift tildelte personer 1-9 for kort (i den rækkefølge de blev tilføjet til tavlen).",
"toggle-labels": "Skift etiketter 1-9 for kortet. Multi-Selection lægger etiketterne 1-9 til",
"remove-labels-multiselect": "Multi-Selection sletter etiketterne 1-9",
"tracking": "Sporing",
"tracking-info": "Du vil få notifikation om alle ændringer i kort som du har oprettet eller er medlem af.",
"type": "Skriv",
"unassign-member": "Fjern medlemstilknytning",
"unsaved-description": "Du har en beskrivelse som ikke er gemt",
"unwatch": "Ophør med at følge",
"unwatch": "Stop med at følge",
"upload": "Overfør",
"upload-avatar": "Overfør en avatar",
"uploaded-avatar": "Overførte en avatar",
"uploading-files": "Laddar upp filer",
"upload-failed": "Uppladdning misslyckades",
"upload-completed": "Uppladdning slutförd",
"custom-top-left-corner-logo-image-url": "Länk till anpassad logotypbild",
"custom-top-left-corner-logo-link-url": "Länk för anpassad logotyp",
"uploading-files": "Uploader filer",
"upload-failed": "Uploading mislykkedes",
"upload-completed": "Uploading fuldført",
"custom-top-left-corner-logo-image-url": "URL til brugerdefineret logobillede øverst til venstre",
"custom-top-left-corner-logo-link-url": "URL til brugerdefineret logolink øverst til venstre",
"custom-top-left-corner-logo-height": "Anpassad logotyphöjd uppe till vänster. Standard: 27",
"custom-login-logo-image-url": "Länk till bild för anpassad logotyp på inloggningssidan",
"custom-login-logo-link-url": "Länk för anpassad logotyp på inloggningssidan",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sortering er slået til",
"cardsSortPopup-title": "Sortera kort",
"due-date": "Förfallodatum",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Serverfel",
"server-error-troubleshooting": "Skicka in felet som inträffade på servern.\nFör snap-installationer, kör: `sudo snap logs wekan.wekan`\nFör Docker-installationer, kör: `sudo docker logs wekan-app`",
"title-alphabetically": "Titel (Alfabetisk)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Vedhæftninger",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Fil",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Skriv",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS aktiverat",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Använd MongoDB GridFS för fillagring",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Misslyckades med att stoppa migreringar",
"migration-stopped": "Migreringar har stoppats",
"mongodb-gridfs-storage": "MongoDB GridFS-lagring",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Pausa alla migreringar",
"s3-access-key": "S3-åtkomstnyckel",
"s3-access-key-description": "AWS S3-åtkomstnyckel för autentisering",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mitglieder zuordnen",
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sortierung ist aktiviert",
"cardsSortPopup-title": "Sortiere Karten",
"due-date": "Fälligkeitsdatum",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server-Fehler",
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
"title-alphabetically": "Überschrift (alphabetisch)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Anhänge",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Datei",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Typ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS aktiviert",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Benutze MongoDB GridFS als Dateispeicher",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
"migration-stopped": "Migrationen erfolgreich gestoppt",
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Alle Migrationen anhalten",
"s3-access-key": "S3 Zugriffsschlüssel",
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mitglieder zuordnen",
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sortierung ist aktiviert",
"cardsSortPopup-title": "Sortiere Karten",
"due-date": "Fälligkeitsdatum",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server-Fehler",
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
"title-alphabetically": "Überschrift (alphabetisch)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Anhänge",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Datei",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Typ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS aktiviert",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Benutze MongoDB GridFS als Dateispeicher",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
"migration-stopped": "Migrationen erfolgreich gestoppt",
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Alle Migrationen anhalten",
"s3-access-key": "S3 Zugriffsschlüssel",
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
+140 -3
View File
@@ -447,7 +447,7 @@
"error-board-doesNotExist": "Dieses Board existiert nicht",
"error-board-notAdmin": "Um das zu tun, müssen Sie Administrator dieses Boards sein",
"error-board-notAMember": "Um das zu tun, müssen Sie Mitglied dieses Boards sein",
"error-notAllowed": "Your board role is not allowed to do that",
"error-notAllowed": "Ihre Board Rolle lässt diese Aktion nicht zu",
"error-json-malformed": "Ihre Eingabe ist kein gültiges JSON",
"error-json-schema": "Ihre JSON-Daten enthalten nicht die gewünschten Informationen im richtigen Format",
"error-csv-schema": "Deine CSV(Kommatgetrennte Werte)/TSV (Tab getrennte Werte) enthält keine korrekten Informationen im geforderten Format ",
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mitglieder zuordnen",
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
@@ -741,7 +784,7 @@
"wipLimitErrorPopup-dialog-pt2": "Bitte verschieben Sie einige Aufgaben aus dieser Liste oder setzen Sie ein grösseres WIP-Limit.",
"admin-panel": "Administration",
"settings": "Einstellungen",
"attachment-transfer-limits-title": "Attachment And API File Size Limits",
"attachment-transfer-limits-title": "Anhang und API Dateigrößen Limits",
"attachment-limits": "Limits",
"attachment-transfer-limits-description": "Configure maximum file size limits for uploads and downloads. API transfer buffering has separate server-side safety caps.",
"attachment-transfer-limits-saved": "Attachment transfer limits saved.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sortierung ist aktiviert",
"cardsSortPopup-title": "Sortiere Karten",
"due-date": "Fälligkeitsdatum",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server-Fehler",
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
"title-alphabetically": "Überschrift (alphabetisch)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Anhänge",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Datei",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Typ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS aktiviert",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Benutze MongoDB GridFS als Dateispeicher",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
"migration-stopped": "Migrationen erfolgreich gestoppt",
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Alle Migrationen anhalten",
"s3-access-key": "S3 Zugriffsschlüssel",
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
+138 -1
View File
@@ -550,6 +550,49 @@
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
"import-json-file": "Or select a Trello JSON file",
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
"import-trello-json-file": "Trello .json file",
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
"import-trello-zip-file": "Trello .zip file",
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
"import-trello-failed": "Importing from Trello failed.",
"import-trello-zip-failed": "Importing the .zip failed.",
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
"import-trello-zip-too-large": "The .zip is too large to import.",
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
"import-trello-workspace": "Personal workspace name (optional)",
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
"import-trello-parent-workspace": "Place imported workspaces under",
"trello-api-import": "Import directly from Trello with API key and token",
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
"trello-api-token": "Trello API token (manually generated below the API key)",
"trello-list-workspaces": "List Trello workspaces",
"trello-parent-workspace-top": "Top level",
"trello-import-selected": "Import selected boards",
"trello-importing": "Importing boards…",
"trello-import-results": "Import results",
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
"select-all": "Select all",
"unselect-all": "Unselect all",
"trello-import-more": "Import more boards",
"trello-import-progress": "Import progress",
"trello-cancel": "Cancel import",
"trello-cancel-delete": "Cancel and delete imported",
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
"trello-resume": "Resume import",
"trello-delete-imported": "Delete imported boards",
"trello-clear-job": "Clear",
"trello-import-errors": "Import errors",
"copy-to-clipboard": "Copy to clipboard",
"running": "Running",
"paused": "Paused",
"import-map-members": "Mitglieder zuordnen",
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
@@ -1261,6 +1304,32 @@
"sort-is-on": "Sortierung ist aktiviert",
"cardsSortPopup-title": "Sortiere Karten",
"due-date": "Fälligkeitsdatum",
"due-complete": "Due complete",
"card-mark-complete": "Mark as complete",
"card-mark-incomplete": "Mark as incomplete",
"stickers": "Stickers",
"add-sticker": "Add sticker",
"remove-sticker": "Remove sticker",
"board-backgrounds": "Board backgrounds",
"boardBackgrounds-title": "Board backgrounds",
"upload-background": "Upload background image",
"set-as-active": "Set as board background",
"background-too-big": "Background image is too big (Maximum {{size}}).",
"board-background-delete-pop": "Are you sure you want to delete this background image?",
"location": "Location",
"location-open-map": "Open in map",
"add-location": "Add location",
"cardLocationsPopup-title": "Add location",
"location-name": "Location name",
"location-address": "Address",
"location-latitude": "Latitude",
"location-longitude": "Longitude",
"location-detect-from-map": "Detect location from any map link",
"location-detect": "Detect",
"location-detect-none": "Could not detect a location from this link.",
"location-detect-done": "Filled in the detected location details below.",
"location-open-map-at": "Open map links at",
"map-provider-saved": "Default map service saved.",
"server-error": "Server-Fehler",
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
"title-alphabetically": "Überschrift (alphabetisch)",
@@ -1377,6 +1446,24 @@
"move-storage-fs": "Filesystem",
"move-storage-s3": "S3/MinIO",
"move-all-attachments": "Move all attachments",
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
"attachment-last-move": "Last move",
"attachment-repair-locations": "Repair file locations",
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
"attachment-repair-running": "Searching and repairing…",
"attachment-repair-done": "Repair finished.",
"attachment-repair-scanned": "Searched",
"attachment-repair-repaired": "Repaired",
"attachment-repair-broken": "Not found",
"move-scope": "Move",
"move-scope-attachments": "Anhänge",
"move-scope-avatars": "Avatars",
"move-scope-both": "Attachments and Avatars",
"move-storage-all": "All Read-enabled storages",
"default-save-storage": "Default Save Storage",
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
"default-save-storage-saved": "Default save storage saved",
"default-save-storage-save-failed": "Failed to save default storage",
"move-progress-file": "Datei",
"move-progress-pause": "Pause",
"move-progress-resume": "Resume",
@@ -1385,7 +1472,7 @@
"calculating-counts": "Calculating counts...",
"stats-scope": "Typ",
"stats-collectionfs": "CollectionFS",
"stats-mongo-files": "Mongo-Files",
"stats-mongo-files": "Meteor-Files",
"stats-count": "Count",
"avatars": "Avatars",
"card-id": "Card ID",
@@ -1532,6 +1619,55 @@
"filesystem-disabled": "Filesystem Storage Disabled",
"filesystem-enabled-description": "Use local filesystem for file storage",
"storage-read": "Read",
"storage-enabled": "Enabled",
"s3-minio-storage-description": "S3-compatible object storage: AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces and others.",
"s3-force-path-style": "Force path-style URLs",
"s3-force-path-style-description": "Required by MinIO and most non-AWS S3-compatible servers.",
"azure-blob-storage": "Azure Blob Storage",
"azure-blob-storage-description": "Microsoft Azure Blob Storage.",
"azure-account-name": "Account name",
"azure-account-key": "Account key",
"azure-connection-string": "Connection string",
"azure-connection-string-description": "Optional: use a full connection string instead of account name and key.",
"azure-container": "Container",
"gcs-storage": "Google Cloud Storage",
"gcs-storage-description": "Google Cloud Storage.",
"gcs-project-id": "Project ID",
"gcs-bucket": "Bucket",
"gcs-key-filename": "Service account key file path",
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
"gcs-credentials": "Service account credentials (JSON)",
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
"azure-account-name-description": "The name of your Azure Storage account.",
"azure-container-description": "The blob container to store files in.",
"gcs-project-id-description": "Your Google Cloud project id.",
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
"cloud-secret-set": "(saved — leave blank to keep)",
"cloud-secret-none": "(not set)",
"test-cloud-connection": "Test connection",
"cloud-connection-success": "Connection successful",
"cloud-connection-failed": "Connection failed",
"cloud-settings-saved": "Storage settings saved",
"cloud-settings-save-failed": "Failed to save storage settings",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Move attachment to Azure Blob Storage",
"attachment-move-storage-gcs": "Move attachment to Google Cloud Storage",
"gridfs-enabled": "GridFS aktiviert",
"gridfs-disabled": "GridFS Disabled",
"gridfs-enabled-description": "Benutze MongoDB GridFS als Dateispeicher",
@@ -1555,6 +1691,7 @@
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
"migration-stopped": "Migrationen erfolgreich gestoppt",
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
"pause-all-migrations": "Alle Migrationen anhalten",
"s3-access-key": "S3 Zugriffsschlüssel",
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
Loaded 100 of 351 files, more files were not shown because too many files have changed in this diff. Show more