Compare commits

..
171 Commits
Author SHA1 Message Date
github-actions[bot] 246ba44838 Bump versions for v13.01 2026-11-07 23:51:55 +00:00
Lauri Ojansivu 1eda4f256e Prepare v13.01 release 2026-11-08 01:49:05 +02:00
Lauri Ojansivu f83ee4d320 CHANGELOG: Dependabot's updates #6879-#6886, checked and kept.
Thanks to xet7 !
2026-11-08 01:48:20 +02:00
Lauri Ojansivu e43dc0070f CHANGELOG: translations and the S3 client; the Cloud Panel is Weddy's.
Thanks to xet7 !
2026-11-08 00:38:40 +02:00
Lauri Ojansivu 579de7d6af The Cloud Panel is Weddy's, not WeKan's: WeKan stays Meteor 3.
Admin Panel / People / Cloud and the Cloud Panel (73db2a28c3) are removed from
WeKan, with their methods, route, menu item, docs and tests. Many domains from
one Weddy is a Weddy feature: there every domain's WeKan is in one process,
where Node.js WeKan containers would have to work together, which gets
complicated. It is written in Go in caddy-wekan-plugin.

The translations of its strings stay: WeKan, Weddy and the rest use the same
WeKan translations.

Thanks to xet7 !
2026-11-08 00:38:40 +02:00
Lauri Ojansivu fb8021ba96 Cloud Panel docs: Weddy runs WeKan in Go, data instead of the Caddy v1-looking root.
The Caddyfile example used `root /path`, which tests/caddyfileExamplesV2.test.cjs
rightly reads as Caddy v1. Weddy's wekan option is now `data`, and WeKan runs
inside Weddy in Go - no bundle, no WEDDY_TENANT - so the page says so, and that
the Cloud Panel's files are written by hand or by a Meteor WeKan until its Go
pages are done.

Thanks to xet7 !
2026-11-08 00:21:56 +02:00
Lauri Ojansivu fe0e187fa0 Translations: Moroccan Arabic is Darija, not Persian.
374 values of ary.i18n.json were Persian - "افزودن مسیر شنا" for add-swimlane,
"برد" for board, "دوشنبه" for Monday - or Persian words inside Arabic text, or
English left in ("And __count__ other بطاقة"). The locale tag is authoritative,
so they are Darija in Arabic script now, with the file's own vocabulary: لوحة,
بطاقة, مسار, قائمة, زيد, حيّد, عدّل. The outdated Todoist and Planner import
instructions and two flow notes follow the current English.

tests/moroccanArabicNotPersian.test.cjs scans every value for words only
Persian has, pins repaired values, proves the scan catches the old strings and
checks every placeholder against English.

Low confidence, for a speaker to correct: the colour names (lime, orange,
magenta, plum, saddlebrown, navy), prefix-with-parent and subtext-with-*,
bidirectional-webhooks, impersonate-user, flow-active, predicate-quarter, r-is
and operator-has.

Thanks to xet7 !
2026-11-08 00:21:29 +02:00
Lauri Ojansivu d1d3beb12d Translations: Sardinian is Sardinian, not Italian.
1772 values of sc.i18n.json were Italian or Italian/Sardinian mixtures
("Il monitoraggio di tàulas e cartas è disattivadu dall'amministradore"); the
locale tag is authoritative, so they are now Sardinian (LSC), from the file's
own correct values and dictionaries. Stray "Elementu: " prefixes are gone, and
values that translated an older English string follow the current one.

tests/sardinianTranslationProgress.test.cjs pins repaired values and scans every
value for words that exist only in Italian; a negative check proves the scan
still catches the old strings.

Low confidence, for a speaker to correct: smtp-port/s3-port, owner, next and
previous, stickers, welcome-swimlane, impersonate-user, speedReportTitle and
the one-word operator-/predicate- search terms.

Thanks to xet7 !
2026-11-08 00:19:58 +02:00
Lauri Ojansivu e33b3f26d3 Bump @aws-sdk/client-s3 to 3.1161.0, which @aws-sdk/lib-storage 3.1161.0 requires.
The lib-storage update of #6884 needs client-s3 ^3.1161.0 as a peer, and the
lock still had 3.1159.0; tests/dependencySecurityUpdates.test.cjs caught it.
Only AWS SDK packages change version: no new packages, no install scripts.

Thanks to xet7 !
2026-11-08 00:14:38 +02:00
Lauri Ojansivu 1211221a0f Translations: fill the remaining English placeholders in 33 languages.
Aragonese, Moroccan Arabic, Bislama, Kashubian, Welsh (cy, cy-GB), Friulian,
Upper Sorbian, Armenian, Javanese, Georgian, Kazakh, Khmer (km, km_KH),
Konkani, Luxembourgish, Ladin, Maithili, Mongolian, Occitan, Odia, Punjabi,
Romansh, Sardinian, Shona, Somali, Silesian, Telugu, Tajik, Turkmen, Tagalog,
Tok Pisin and Tatar: every string still equal to English, including the Cloud
Panel strings, written directly in the language from its existing translations
and kanban terms. Only English placeholders were filled; human translations
were not touched, and placeholders are copied from en.i18n.json.

Low confidence, for a speaker to correct: Bislama, Kashubian, Silesian, Ladin,
Friulian, Sardinian, Moroccan Arabic, Konkani, Maithili, Shona, Somali, Tok
Pisin, Turkmen, Tajik and Odia.

Thanks to xet7 !
2026-11-08 00:09:47 +02:00
Lauri Ojansivu c00c602441 CHANGELOG: repoint commit links after history moved.
A rebase rewrote the commits these entries link to, so the links named commits
that no longer exist and would have 404ed on GitHub. Repointed by
releases/fix-changelog-hashes.sh, which only touches a link no ref in this clone
can reach.

Thanks to xet7 !
2026-11-08 00:07:09 +02:00
Lauri Ojansivu 47a19e3842 CHANGELOG: the Cloud Panel, and companion repositories in .tools/w.
Thanks to xet7 !
2026-11-07 23:32:43 +02:00
Lauri Ojansivu 73db2a28c3 Cloud Panel: many WeKan domains from one Weddy, for the SuperAdmin at the Cloud Panel Domain.
Weddy (wekan/caddy-wekan-plugin) runs a WeKan for every domain, each in its own
directory - files/domains/<domain>/files/db/ for its database - and starts
the first domain's WeKan with WEDDY_CLOUD_DIR, every customer's with
WEDDY_TENANT=true.

- Admin Panel / People / Cloud, only in that first WeKan and only for a site
  admin (a per-tenant admin's whitelisted menu never has it): Cloud Panel
  Domain, e.g. cloud.wekan.team, and SuperAdmin Username, a username or
  e-mail address.
- /cloud, the Cloud Panel, where the SuperAdmin is taken at the Cloud Panel
  Domain: Settings with the left menus Domains and CloudFlare, in the Admin
  Panel's layout. Domains is a table page - Domain, Customer name, Customer
  email address, Billing email address, Start Date, End Date and the TLS
  Weddy got - with a form to add and edit. CloudFlare is a write-only API
  key field.
- server/methods/cloudPanel.js checks in every method again that the caller is
  the SuperAdmin and the request's Host is the Cloud Panel Domain - not
  X-Forwarded-Host, which a client could send itself. The collections are not
  published. Every change writes WEDDY_CLOUD_DIR/domains.json for Weddy; the
  key goes to cloudflare.json, mode 0600, and is never sent back.
- models/lib/cloudPanel.js has the rules, shared by client and server; its
  domain rule is the same as Weddy's, since the name becomes a directory.
- Customers' WeKans show none of it.

The 15 new strings are added as pending Transifex, written in Finnish, and
translated into every other locale. Low-confidence translations: ace ak an ay
ba bho bi bm bo bua chr co csb cv dz ee ff fj fo fur gn gv haw hsb ht ig iu
kl kok ks kw lg lld mai mi nah nap nd nso oc om pap qu rm rn rup rw sah sc scn
se sm sn so ss st szl tig tlh tn to tpi ts uz-AR ve ve-CC ve-PP vo wa wa-RR
wal wo wuu-Hans yi zgh. Transifex translations replace them when they arrive.

tests/cloudPanel.test.cjs: the rules with their negatives, and the security
properties the source keeps. The node suites pass, 2160 of 2160. The
templates are not compiled here (no Jade compiler in this sandbox); a Meteor
build checks them. docs/Platforms/FOSS/Weddy/Cloud-Panel.md is the design.

Thanks to xet7 !
2026-11-07 23:32:43 +02:00
Lauri Ojansivu 11bed26788 Companion repositories are found in .tools/w, .tools/xet7 and .tools/Secretchronicles.
The repositories under .tools/ are now grouped by their GitHub owner:
github.com/wekan/* in .tools/w/, github.com/xet7/* in .tools/xet7/ and
github.com/Secretchronicles/* in .tools/Secretchronicles/. build.sh,
build.bat, the release and debug scripts, the tests and the docs looked
for .tools/FerretDB, .tools/wekan.fi, .tools/charts, .tools/node-patches,
.tools/mongo-tools-patches and .tools/TSC, and now use the new places.

ensure_tool_repo takes the owner from the clone URL through a new
tool_repo_dir, so a missing companion repository is cloned into the same
place the scripts read it from. tests/toolsDirectoryRepos.test.cjs runs the
mapping and checks that nothing lands flat in .tools/ any more.

Finding the website checkout again let securityRegressionCoverage compare
the Hall of Fame, which it had been skipping: CreateBleed is published and
is now in the inventory with its unit and UI tests. The exceljs fork test
reads npm-packages/exceljs, the copy WeKan builds with, instead of a clone
under .tools that is not part of the build.

Thanks to xet7 !
2026-11-07 23:32:43 +02:00
Lauri Ojansivu 6a64d374c6 Merge pull request #6886 from wekan/dependabot/npm_and_yarn/mailparser-3.14.2
Bump mailparser from 3.12.4 to 3.14.2
2026-11-07 23:32:07 +02:00
Lauri Ojansivu 8f15879816 Merge pull request #6885 from wekan/dependabot/npm_and_yarn/isomorphic-git-1.44.10
Bump isomorphic-git from 1.44.8 to 1.44.10
2026-11-07 23:31:40 +02:00
Lauri Ojansivu 74339a8a82 Merge pull request #6884 from wekan/dependabot/npm_and_yarn/aws-sdk/lib-storage-3.1161.0
Bump @aws-sdk/lib-storage from 3.1155.0 to 3.1161.0
2026-11-07 23:31:15 +02:00
Lauri Ojansivu 36a1939653 Merge pull request #6883 from wekan/dependabot/npm_and_yarn/filesize-11.0.30
Bump filesize from 11.0.28 to 11.0.30
2026-11-07 23:30:52 +02:00
Lauri Ojansivu a61a40a5db Merge pull request #6882 from wekan/dependabot/npm_and_yarn/pdfjs-dist-6.5.438
Bump pdfjs-dist from 6.4.299 to 6.5.438
2026-11-07 23:30:33 +02:00
Lauri Ojansivu 4fd3217aee Merge pull request #6881 from wekan/dependabot/github_actions/helm/chart-releaser-action-1.9.0
Bump helm/chart-releaser-action from 1.8.0 to 1.9.0
2026-11-07 23:30:18 +02:00
Lauri Ojansivu 5b436a18dc Merge pull request #6880 from wekan/dependabot/npm_and_yarn/tests/playwright/mongodb-7.8.0
Bump mongodb from 7.7.0 to 7.8.0 in /tests/playwright
2026-11-07 23:30:03 +02:00
Lauri Ojansivu 2e87a775ae Merge pull request #6879 from wekan/dependabot/github_actions/vmactions/netbsd-vm-1.5.3
Bump vmactions/netbsd-vm from 1.5.2 to 1.5.3
2026-11-07 23:29:45 +02:00
dependabot[bot] f653d619f5 Bump mailparser from 3.12.4 to 3.14.2
Bumps [mailparser](https://github.com/nodemailer/mailparser) from 3.12.4 to 3.14.2.
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.12.4...v3.14.2)

---
updated-dependencies:
- dependency-name: mailparser
  dependency-version: 3.14.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:33:40 +00:00
dependabot[bot] da731aa2fe Bump isomorphic-git from 1.44.8 to 1.44.10
Bumps [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) from 1.44.8 to 1.44.10.
- [Release notes](https://github.com/isomorphic-git/isomorphic-git/releases)
- [Commits](https://github.com/isomorphic-git/isomorphic-git/compare/v1.44.8...v1.44.10)

---
updated-dependencies:
- dependency-name: isomorphic-git
  dependency-version: 1.44.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:33:31 +00:00
dependabot[bot] a42b63d711 Bump @aws-sdk/lib-storage from 3.1155.0 to 3.1161.0
Bumps [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) from 3.1155.0 to 3.1161.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1161.0/lib/lib-storage)

---
updated-dependencies:
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.1161.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:33:22 +00:00
dependabot[bot] a14923cac3 Bump filesize from 11.0.28 to 11.0.30
Bumps [filesize](https://github.com/avoidwork/filesize.js) from 11.0.28 to 11.0.30.
- [Changelog](https://github.com/avoidwork/filesize.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/avoidwork/filesize.js/compare/11.0.28...11.0.30)

---
updated-dependencies:
- dependency-name: filesize
  dependency-version: 11.0.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:33:03 +00:00
dependabot[bot] fdcb26096b Bump pdfjs-dist from 6.4.299 to 6.5.438
Bumps [pdfjs-dist](https://github.com/mozilla/pdf.js) from 6.4.299 to 6.5.438.
- [Release notes](https://github.com/mozilla/pdf.js/releases)
- [Commits](https://github.com/mozilla/pdf.js/compare/v6.4.299...v6.5.438)

---
updated-dependencies:
- dependency-name: pdfjs-dist
  dependency-version: 6.5.438
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:32:55 +00:00
dependabot[bot] b59152a079 Bump helm/chart-releaser-action from 1.8.0 to 1.9.0
Bumps [helm/chart-releaser-action](https://github.com/helm/chart-releaser-action) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/helm/chart-releaser-action/releases)
- [Commits](https://github.com/helm/chart-releaser-action/compare/df287104677278f869ca5480bd260358397cc3f9...92c6b97fa9f664492a2592e0d1d80bc6bf378957)

---
updated-dependencies:
- dependency-name: helm/chart-releaser-action
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:32:46 +00:00
dependabot[bot] 1595155af2 Bump mongodb from 7.7.0 to 7.8.0 in /tests/playwright
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 7.7.0 to 7.8.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v7.7.0...v7.8.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-version: 7.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:32:44 +00:00
dependabot[bot] f4f4c7f2cb Bump vmactions/netbsd-vm from 1.5.2 to 1.5.3
Bumps [vmactions/netbsd-vm](https://github.com/vmactions/netbsd-vm) from 1.5.2 to 1.5.3.
- [Release notes](https://github.com/vmactions/netbsd-vm/releases)
- [Commits](https://github.com/vmactions/netbsd-vm/compare/c8a0d7ddb84619a7a8cc4e652efa7950b3fd9f92...5957cf330aef2e6ddc63c604364c99ce2e02b91d)

---
updated-dependencies:
- dependency-name: vmactions/netbsd-vm
  dependency-version: 1.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-07 20:32:39 +00:00
github-actions[bot] 97cc91cd67 Bump versions for v13.00 2026-11-06 23:05:04 +00:00
Lauri Ojansivu 33c2ad8fb0 Prepare v13.00 release 2026-11-07 01:03:53 +02:00
Lauri Ojansivu c936e7708d CHANGELOG: CreateBleed, only admins create boards on every path.
Thanks to tsurutanmen and xet7 !
2026-11-07 00:53:47 +02:00
Lauri Ojansivu 5c1f6088f4 Fix CreateBleed: "Only admins can create boards" holds on every path.
GHSA-qcfm-2g4w-wqcc. Admin Panel / Settings / Visibility / "Only admins
can create boards" (#4475) was checked only inside
createBoardWithInitialSwimlanes, the Add Board form. A non-admin still
created working boards by a client Boards.insert (Boards.allow.insert
approves any signed-in user), POST /api/boards and both board import
routes, as reported, and also by importBoard, cloneBoard, copyBoard,
POST /api/boards/:boardId/copy and createBoardFromCard.

One decision (models/lib/boardCreationPolicy.js) and one guard
(server/lib/boardCreationGuard.js) are asked by every one of those paths
before they write, and by a new Boards.deny insert rule. A board template
stays every member's own; boards WeKan makes itself (a Templates container,
a board's subtasks board) are not created by the user. A REST refusal is
403. A refusal is recorded in Admin Panel / Problems as CreateBleed
(authz.board-creation, low, CWE-863, blocks no account), and Duplicate and
"Create board from card" are hidden like Add Board, so only a client
bypassing the interface reaches it.

tests/createBleed.test.cjs drives the decision, the guard and the real deny
rule against the reporter's console insert, and fails if any path that
creates a board for a user does not ask the guard, or if anything else
reads the setting. tests/playwright/specs/createbleed-board-creation-admin-only.e2e.js
replays the report against a running server. The guards that pinned the
old inline check now pin the shared one.

Thanks to tsurutanmen and xet7 !
2026-11-07 00:51:53 +02:00
Lauri Ojansivu ceb20a81fc Tests: keyboard-nonlatin-once skips outside Chromium, and every CDP test must.
keyboard-nonlatin-once sends its key events through the DevTools protocol
and had no skip, so in the full Firefox run it failed with "CDP session is
only available in Chromium" - the same error as card-window-touch-drag's
mouse test (2128e6e02f). It now skips outside Chromium like the other CDP
tests. tests/playwrightCdpChromiumOnly.test.cjs requires that skip on every
test that opens a CDP session, itself or through a helper of its file (a
`touch: false` call opens none, a describe-level skip covers its file), and
finds both tests as they were.

Thanks to xet7 !
2026-11-07 00:14:50 +02:00
Lauri Ojansivu 2128e6e02f Tests: the mouse test of card-window-touch-drag runs in every browser.
openCardWindow always opened a DevTools (CDP) session to emulate touch, and
the mouse test, which needs no touch, called it too: in Firefox it failed
with "CDP session is only available in Chromium" before it had moved
anything. The touch is now optional, and the mouse test asks for none; the
touch tests still skip outside Chromium. It passes in Firefox and in
Chromium.

Thanks to xet7 !
2026-11-06 23:53:09 +02:00
Lauri Ojansivu 49ee37a3a8 Tests: Card's settings menu is opened again until its panel shows.
In the full two-worker Firefox run, board-settings-columns' non-admin test
clicked Card's hamburger right after Board Settings / Card was drawn and
the panel never opened; alone it passes 4 of 4. The button toggles the
panel, so the helper now clicks again only while the panel is still closed,
up to 15 seconds. The whole spec passes in Firefox.

Thanks to xet7 !
2026-11-06 23:27:52 +02:00
Lauri Ojansivu 19350f2611 A code block in a description reads in the dark theme.
.viewer pre and code have their own light background (#dedede) in
layouts.css but inherited the theme's text colour. In the dark theme's
opened card that is #bbb, 1.42:1, and axe-core's color-contrast rule
failed the dark theme in the full Firefox run (46-axe-theme-contrast,
".viewer > pre #bbbbbb/#dedede 1.42"). The block now names its text colour,
#222, 11.8:1 on its background - the background codeHighlight.css's token
colours are chosen for. tests/viewerCodeContrast.test.cjs requires an
explicit text colour of at least 4.5:1 on the block's own background, and
fails on the old rule.

Thanks to xet7 !
2026-11-06 23:22:54 +02:00
Lauri Ojansivu 2306a36814 A long popup menu is laid out before it is drawn, not a frame later.
The column layout of a long menu (Board View, the member menu, the card's
menu) was applied on the animation frame after the menu's contents arrived,
so for a frame the popup was drawn - and could be clicked or dragged - at
its narrow size, then jumped to 960px under the pointer. In the full Firefox
run a click on Board View's "Lists" landed where "Calendar" ends up, the
view stayed Swimlanes and 06-views-layout's #6659/#6660 test failed; the
same jump was behind the popup drag of card-title-edit-and-move-6839. The
MutationObserver callback, which already runs before the next paint, now
applies the layout itself. tests/popupMenuLayout.test.cjs drives the
observer and fails if the layout is deferred to an animation frame again.

Thanks to xet7 !
2026-11-06 23:11:29 +02:00
Lauri Ojansivu a4d2313a68 Password policy specs no longer run side by side or leave a minimum enforced.
In the full Chromium run (two workers) password-length-report.e2e.js and
password-min-length-signup-login.e2e.js ran at the same time. Both write
settings.loginOptions.passwordMinLength and passwordMinLengthEnforce, one
document for every worker, so each turned enforcement on or off under the
other and one test of each failed. Both also restored the loginOptions they
had seen before each test, so once a test was interrupted the snapshot
already held an enforced minimum, and every later test put it back: the
test database was left with a minimum of 10 enforced.

tests/playwright/helpers/sharedSettingLock.js is a lock across workers (an
atomic mkdir under TMPDIR, counted per process, and taken over from a
worker that died); both specs hold 'password-policy' for the whole file,
and put the policy back to its default with $unset after each test. Run
together on two workers both files pass, 6 of 6.
tests/playwrightSharedSettingLock.test.cjs requires the lock and the
$unset in every spec that writes the policy, refuses a restored snapshot,
and drives the lock: shared within a process, released by the last holder,
taken over from a dead one, and waited for from a live one.

Thanks to xet7 !
2026-11-06 22:12:49 +02:00
Lauri Ojansivu 570cc34552 Stopping a timer adds its time to the total, never replaces it.
Flowtime's Stop and Pomodoro's completed or stopped work interval read the
total on the client (getSpentTime()) and $set the sum. For a card that
links to a board or a card, that total is the linked document's, which can
still be on its way to the client: getSpentTime() then read null, and the
Stop replaced a linked board's 4 logged hours with the 1 hour of the
session (linked-timer-sessions.e2e.js failed 4 of 6 repeats). The session
length is now added on the server with addSpentTime(), an $inc on the same
spentTime field of the linked board, the linked card or the card. The
Flowtime and Pomodoro guards check addSpentTime and no longer pin
setSpentTime, and tests/linkedTimerSessions.test.cjs credits correctly
with no total on the client, pins the $inc targets, refuses a zero,
negative or non-number amount, and fails if a timer helper computes a total
from getSpentTime() again.

Thanks to xet7 !
2026-11-06 21:35:36 +02:00
Lauri Ojansivu 7ae45651dc A popup that grows while it is dragged stays inside the window.
A long menu, such as the member menu, gets its 960px column layout a frame
after it is drawn (client/lib/popupMenuLayout.js). A drag begun in that
frame clamped every move with the 380px the popup started at, so the grown
popup was put at left 850, mostly outside a 1280px window - and each move
undid the ResizeObserver that had just put it back inside. The drag now
clamps with the popup's current size. tests/calendarPopupResize.test.cjs
grows the popup mid-drag and fails on the old clamp; the Playwright spec
card-title-edit-and-move-6839 measures a popup only after its box has
settled, and passes 24 of 24 repeats where it failed 3 of 20.

Thanks to xet7 !
2026-11-06 21:00:30 +02:00
Lauri Ojansivu db52764592 Docker Playwright browsers open WeKan at ROOT_URL, so uploads work in tests.
build.sh starts the test server with ROOT_URL=http://localhost:3000 but sent
the Docker Playwright browsers and the Node E2E container to
http://127.0.0.1:3000. Meteor-Files sends every upload to ROOT_URL +
/cdn/storage/<collection>/__upload and answers no CORS preflight for another
origin, so the browser blocked it ("Access to fetch at
'http://localhost:3000/cdn/storage/attachments/__upload' from origin
'http://127.0.0.1:3000' has been blocked by CORS policy") and the four drawing
specs whose Save uploads an attachment timed out in the full Chromium run.
The default is now http://localhost:3000, like playwright.config.js and the
macOS path; with it the eight drawing tests pass in the same container.
tests/playwrightBaseUrlMatchesRootUrl.test.cjs checks that every browser
default in build.sh and the Playwright config is the test server's ROOT_URL
origin, and fails on the old default.

Thanks to xet7 !
2026-11-06 20:56:02 +02:00
Lauri Ojansivu 2b99ffe288 Source-scanning tests skip every _build* directory, not only _build.
After the Mocha stage of a full test run had compiled _build-local-test/,
tests/publishFilesCursor.test.cjs read server-rspack.js there and failed
on nine lines of bundled code. 62 tests walked the tree skipping _build by
exact name, so each of them read rspack's second copy of every source
file whenever a test build was present. They now skip by the _build
prefix, as tests/noIdentityReplacement.test.cjs already did, and
tests/sourceWalkersSkipBuildOutput.test.cjs fails on any walker that
skips _build by exact name only.

Thanks to xet7 !
2026-11-06 20:20:10 +02:00
Lauri Ojansivu b4b130858f CHANGELOG: built API page spells the name WeKan.
Thanks to xet7 !
2026-11-06 19:43:28 +02:00
Lauri Ojansivu 0275363043 The built API page public/api/wekan.html spells the name WeKan.
The v12.99 version bump rebuilt public/api/wekan.html with the generator
from before the spelling fix, and releases/release-website.sh copies it
to wekan.fi as api/v12.99/index.html, so that page would have said
Wekan again. Its visible text and the Redoc JSON now say WeKan, and
tests/openapiBrandSpelling.test.cjs checks the built page too.

Thanks to xet7 !
2026-11-06 19:43:20 +02:00
Lauri Ojansivu 42d8341d47 Merge remote-tracking branch 'origin/main' 2026-11-06 19:42:55 +02:00
Lauri Ojansivu a9aaa22592 CHANGELOG: REST API documentation spells the name WeKan.
Thanks to xet7 !
2026-11-06 19:25:20 +02:00
Lauri Ojansivu 4d5fe63fc4 The name is WeKan in the REST API documentation.
openapi/generate_openapi.py's header and the models' JSDoc said
"Wekan REST API", "extend Wekan", "a production Wekan server" and
"in the Wekan board", so every release's API pages on wekan.fi failed
its brand-spelling check (v12.95-v12.97 did). The generator, the model
comments and public/api/wekan.yml now say WeKan, and
tests/openapiBrandSpelling.test.cjs checks the generated file, the
generator header and every model JSDoc block.

Thanks to xet7 !
2026-11-06 19:24:04 +02:00
github-actions[bot] 1f12b20291 Bump versions for v12.99 2026-11-06 17:05:44 +00:00
Lauri Ojansivu bf8ecbbc19 Prepare v12.99 release 2026-11-06 19:04:35 +02:00
Lauri Ojansivu e89a3450cd CHANGELOG: mac-arm64 on macos-15, weba instructions moved.
Thanks to xet7 !
2026-11-06 18:57:40 +02:00
Lauri Ojansivu 7e4483b1a9 Build mac-arm64 release bundle on macos-15, macos-14 runner is retired.
The v12.98 release run's build-mac-arm64 job was cancelled with
"The configuration 'macos-14-arm64-us-default' is not supported",
so no mac-arm64 bundle was built. macos-15 is Apple Silicon too and
is what mac.yml already uses. tests/workflowRunnerLabels.test.cjs now
lists macos-14 as retired and checks that build-mac-arm64 asks for a
current Apple Silicon runner.

Thanks to xet7 !
2026-11-06 18:52:56 +02:00
Lauri Ojansivu a4eacfd9bf Merge remote-tracking branch 'origin/main' 2026-11-06 17:02:46 +02:00
Lauri Ojansivu b4cb989391 Move weba instructions into its own repository
Remove the weba-specific section from the instruction twins now that the companion repository maintains its own scoped AGENTS.md and CLAUDE.md.
2026-11-06 17:01:28 +02:00
github-actions[bot] 60377a4dc2 Bump versions for v12.98 2026-11-06 14:54:56 +00:00
Lauri Ojansivu 5c45d55d70 Prepare v12.98 release 2026-11-06 16:53:41 +02:00
Lauri Ojansivu e5a84cbcef CHANGELOG: weba in AGENTS.md and CLAUDE.md.
Thanks to xet7 !
2026-11-06 16:51:08 +02:00
Lauri Ojansivu 1ef7a57ce4 Added weba info to AGENTS.md and CLAUDE.md.
Thanks to xet7 !
2026-11-06 16:49:47 +02:00
Lauri Ojansivu 9caf0c8653 CHANGELOG: notificationAttachments publication.
Thanks to xet7 !
2026-11-06 16:42:30 +02:00
Lauri Ojansivu adb73fd8ee Notifications drawer: notificationAttachments publishes a cursor again.
Every subscription failed on the server with "Exception from sub
notificationAttachments ... Publish function can only return a Cursor or an
array of Cursors". Attachments is an ostrio:files FilesCollection, so
ReactiveCache.getAttachments(..., true) answers with a FilesCursor; the
publication returned that instead of the Mongo cursor it wraps, and the
drawer never received the attachments its activities name. It now returns
`ret.cursor || ret`, as every other attachment publication does.

tests/publishFilesCursor.test.cjs runs the publication with a FilesCursor
and requires the Mongo cursor back, and scans every publishing file so no
getAttachments/getAvatars cursor query is published without its .cursor.

Thanks to xet7 !
2026-11-06 16:42:21 +02:00
Lauri Ojansivu ea5724babd CHANGELOG: REST card update sort, isOverTime and description.
Also points the Upcoming entries at their commits after the pull rebased them.

Thanks to AhmedLukman and xet7 !
2026-11-06 16:24:40 +02:00
Lauri Ojansivu b895e61237 REST card update: sort is a number, isOverTime can be turned off, a description can be cleared.
#6877 made the card PUT write `sort` whenever it is present, so 0 works -
and so would null, '' or 'abc', written as the card's position. The same
handler read `isOverTime` and `description` by truthiness, so false could
not be set and an empty description could not clear the old one.

parseCardScalarFields() in server/lib/restCardHelpers.js now decides all
three by presence and checks them before the first write: sort must be a
finite number (a form-encoded '0' becomes 0), isOverTime true/false or
'true'/'false', description a string (null clears it). Anything else is a
400 and the card is left as it was, rather than half of the edit applied.

tests/restCardSort.test.cjs drives the handler for the accepted values,
refuses the invalid ones with no writes, and fails if any of the three is
read by truthiness again.

Thanks to AhmedLukman and xet7 !
2026-11-06 16:24:31 +02:00
Lauri Ojansivu 3108e017fc CHANGELOG: public CSV export fields, REST card 404s, test guards.
Also points the GroupLinkBleed entry at its commit after the pull rebased it.

Thanks to AhmedLukman and xet7 !
2026-11-06 16:15:34 +02:00
Lauri Ojansivu 081690875d Tests: the real single executable test says why it cannot run on a kernel with large pages.
On Linux, --build-sea injects the SEA blob with LIEF, which adds PT_LOAD
segments aligned to 4 KiB. A kernel with 16 KiB pages (Apple-silicon Linux)
or 64 KiB pages (some ARM servers) cannot map them, and the executable dies
with SIGSEGV before it runs a line. A one-line console.log SEA built with
Node.js v26.13.0 crashes the same way, so it is the injection, not WeKan.
The test failed there as `null !== 0`, which said nothing; it is now skipped
when the page size is over 4 KiB, with that reason. Release builders have
4 KiB pages, so they still run it.

Thanks to xet7 !
2026-11-06 16:15:34 +02:00
Lauri Ojansivu 68b575637d Tests: three client guards follow the code they pin again.
The behaviour did not change; each guard read source that later commits
moved, and failed on what it could no longer find:

- sidebarBelowHeader: Template.header.onRendered also wires the full
  screen button since f25957dcf3, and the slice now ran into the new
  onDestroyed. The slice ends at onDestroyed, and the fake environment has
  an inert Fullscreen API.
- maximizedCardPosition: since efaf94242e the maximized selector is also in
  a narrow-screen @media rule that only moves `top` below the header bar on
  a phone. The regex found that rule first; it now reads the top-level one.
- titleClickToEdit: since 1b05673200 the opened card moves with Pointer
  Events, and what a header press does is cardHeaderPress() in
  models/lib/windowDrag.js. The guards read the pointerdown handler and call
  that decision, keeping every assertion: the title edits, links stay links,
  the drag handle wins when drag handles are on, the editor is never dragged.

Each still fails when the behaviour it pins is removed (checked by deleting
onTitleEditZone and by making the maximized card absolute).

Thanks to xet7 !
2026-11-06 16:15:34 +02:00
Lauri Ojansivu 6d5f660c27 REST custom-field update answers JSON 404 for an unmatched card instead of 500.
POST /api/boards/:boardId/lists/:listId/cards/:cardId/customFields/:customFieldId
threw Meteor.Error(404) when no unarchived card matched the board, list and
card id. A WebApp handler is a plain connect handler, so that reached the
client as HTTP 500. It now answers through sendJsonResult with a JSON 404,
as the card archive/restore endpoints do since #6876.

tests/restHandlerNotFound.test.cjs drives the handler for a matching card,
a missing card, the wrong list, the wrong board and an archived card, and
scans every WebApp.handlers route in the source tree so no REST handler
can answer not-found by throwing Meteor.Error(404) again.

Thanks to AhmedLukman and xet7 !
2026-11-06 16:15:34 +02:00
Lauri Ojansivu f54aa93dc1 CHANGELOG: GroupLinkBleed.
Thanks to bhaswanthc and xet7 !
2026-11-06 16:15:34 +02:00
Lauri Ojansivu e3458004ec Fix GroupLinkBleed: board admins can no longer attach foreign teams or add members by direct board writes.
GHSA-v544-rmr5-38gx. Boards.allow lets a board admin update any field of
their board, and two checks existed only inside Meteor methods: a board
admin may add only the organizations and teams they belong to (#5141,
setBoardOrgs/setBoardTeams), and inviteUserToBoard's checks (who may
invite, disabled accounts, the #6116 same organization/team policy). A
client calling /boards/update or /boards/insert directly skipped both.

New Boards.deny rules refuse any client insert or update naming orgs,
teams or domains - the UI changes them only through the board methods,
which write server-side - and any client update that adds a member or
switches a removed member back on (models/lib/boardWriteGuards.js). Role
changes and removals, which the client writes itself, still pass. A
refusal is recorded in Admin Panel / Problems as GroupLinkBleed
(authz.group-link, authz.member-add, medium).

tests/groupLinkBleed.test.cjs runs the real deny rules in a vm against
the reported writes and every legitimate client write, and fails if a
method-guarded board field is client-writable; it fails without the fix.
tests/playwright/specs/group-link-bleed.e2e.js replays the attack over DDP.

Thanks to bhaswanthc and xet7 !
2026-11-06 16:15:34 +02:00
Lauri Ojansivu 6ae4ad3545 Merge pull request #6877 from AhmedLukman/codex/rest-card-sort-zero
Allow zero sort values in REST card updates
2026-11-06 16:15:10 +02:00
Ahmed Lukman b0a71ba57b Allow zero sort values in REST card updates
Check whether sort is supplied so JSON updates can persist a zero position. Keep the existing scoped update and history handling, and add handler and browser regressions for ordering, persistence, omitted values and rejected requests.
2026-11-06 16:54:07 +03:00
Lauri Ojansivu f3d013339c Merge pull request #6876 from AhmedLukman/codex/rest-card-archive-not-found
Fix 404 responses for card archive and restore
2026-11-06 15:42:23 +02:00
Lauri Ojansivu 95ea04b049 Merge pull request #6875 from AhmedLukman/codex/public-csv-export-fields
Honor field selections in public CSV and TSV exports
2026-11-06 15:42:08 +02:00
Ahmed Lukman 3b33c7d7ba Return JSON 404 for unmatched card archive and restore requests
Use the existing JSON response helper when the board, list, card and archive-state selector finds no card. Preserve authorization checks and the existing archive and restore methods.

Add handler and browser regressions for missing cards, incorrect scopes, wrong states, repeated requests, successful transitions and unauthorized callers.

Thanks to AhmedLukman and xet7 !
2026-11-06 14:34:33 +03:00
Ahmed Lukman 4762bed8c1 Honor field selections in public CSV and TSV exports
Share validated export options between public and private CSV routes. Add handler regressions and live export-menu coverage for comma, semicolon and tab delimiters, default columns and private-board access.
2026-11-06 12:20:48 +03:00
github-actions[bot] 029ecb6b39 Bump versions for v12.97 2026-11-06 02:01:17 +00:00
Lauri Ojansivu 49c309b324 Prepare v12.97 release 2026-11-06 04:00:13 +02:00
Lauri Ojansivu e09267cc90 CHANGELOG: BSD smoke test cleanup and snap Launchpad retries.
Thanks to xet7 !
2026-11-06 03:52:17 +02:00
Lauri Ojansivu b46f155fa3 Fix v12.96 release builds: BSD smoke test cleanup and snap Launchpad retries.
openbsd-arm64 bundle: releases/bsd/smoke.sh returned OK with WeKan and
FerretDB still running, because start-wekan.sh runs its TERM trap only
after the foreground node exits. FerretDB then removed wekan.sqlite-wal
and -shm while the VM copyback rsync walked the workspace, and rsync exit
24 failed a bundle that had built and started. smoke.sh now stops node
and FerretDB, waits until both are gone (SIGKILL after 30 s) and removes
its work directory before returning.

s390x snap: all three attempts met Launchpad "HTTP Error 503: Service
Unavailable" / "Error: Timeout", but snapcraft printed only its last
exception, "Could not find repository at path ...", and the 503 was only
in snapcraft's execution log, which infra_failure() did not read. The
attempts were counted as WeKan failures instead of earning the extra
retries. infra_failure() now reads this attempt's execution logs, and
also counts the push refusal "You do not have permission to create
refs/..." from the repository those timed-out requests half-created.

Tests reproduce both from the v12.96 logs, with negatives: an earlier
attempt's execution log and a log with no outage do not count, and no
process or smoke-test file is left behind when smoke.sh returns.

Thanks to xet7 !
2026-11-06 03:52:04 +02:00
Lauri Ojansivu 0c0288a6c5 CHANGELOG: build.sh and releases scripts work on Arch Linux and Omarchy.
Thanks to xet7 !
2026-11-06 01:57:16 +02:00
Lauri Ojansivu c61a1b7f41 build.sh and releases/*.sh work on Arch Linux and Omarchy.
Omarchy (https://omarchy.org) is Arch with Hyprland: pacman, the Arch
repositories and yay for the AUR. The shared detector in
releases/ensure-tools.sh now names Omarchy and the other Arch derivatives
(EndeavourOS, CachyOS, Manjaro) explicitly, and what the scripts did there
is fixed:

- No more `pacman -Sy <package>`: that is a partial upgrade, which Arch does
  not support. _et_pacman installs with `-S --needed`, does a full `-Syu`
  only when pacman has never synced (a fresh archlinux container) or when
  WEKAN_PACMAN_SYU=1 asks for it, and otherwise says to update first, naming
  omarchy-update on Omarchy.
- 7zip instead of p7zip, which Arch has replaced.
- snapcraft: snapd from the AUR with yay or paru (never as root), then
  snapd.socket, /snap and the snapcraft snap, instead of only an error.
- The SSH server menu entries use sshd.service outside Debian/Ubuntu, where
  the unit is not called ssh.
- Playwright: `install --with-deps` runs apt-get, so outside Debian/Ubuntu
  the browser libraries come from pacman (or dnf) and the browsers are
  installed without --with-deps. WebKit, which Playwright builds for
  Debian/Ubuntu only, runs in the Playwright Docker image on Arch when
  Docker is there.
- The en_US.UTF-8 locale note gives the Arch/Omarchy steps too.

tests/archOmarchyScripts.test.cjs drives the helpers offline with stubbed
sudo/pacman/yay, and its negative test fails if any tracked shell script
does a `pacman -Sy` partial upgrade or installs p7zip.

Thanks to xet7 !
2026-11-06 01:57:00 +02:00
github-actions[bot] 9ce35869a7 Bump versions for v12.96 2026-11-05 19:41:40 +00:00
Lauri Ojansivu 2017d07e37 Prepare v12.96 release 2026-11-05 21:40:23 +02:00
Lauri Ojansivu 9ea5c81932 CHANGELOG: repoint commit links after history moved.
A rebase rewrote the commits these entries link to, so the links named commits
that no longer exist and would have 404ed on GitHub. Repointed by
releases/fix-changelog-hashes.sh, which only touches a link no ref in this clone
can reach.

Thanks to xet7 !
2026-11-05 21:39:54 +02:00
Lauri Ojansivu 45ccdd9c3c CHANGELOG: the bundle starts again, and v12.95 records that its bundle did not.
Thanks to xet7 !
2026-11-05 21:39:02 +02:00
Lauri Ojansivu 5a21b074d4 The bundle starts again: nodemailer 10 in meteor/email gets back the lib/ paths it was built against.
releases/bump-bundle-npm-deps.mjs raises the nodemailer inside Meteor's email
package from 8.0.3 to 10.3.1 (dad03959d9), and nodemailer 10 moved lib/ to
dist/cjs/. packages/email.js, compiled against 8.0.3, links
lib/nodemailer.js and lib/well-known/index.js and Npm.requires
lib/mail-composer by path, so v12.95's build-amd64 failed its smoke boot:
"Cannot find module .../meteor/email/node_modules/nodemailer/lib/nodemailer.js".

releases/bundle-npm-security-bumps.json now shims nodemailer the way it
already shims uuid 11: lib/nodemailer.js and every lib/<name> directory the
exports map lists are written back, each forwarding to dist/cjs, and
lib/package.json marks lib/ as CommonJS because nodemailer is "type":
"module". A fresh local bundle taken through build-amd64's install, bump,
trim and prune steps now passes releases/bundle-smoke-boot.sh.

tests/bundleNpmSecurityBumps.test.cjs applies the shim to node_modules'
nodemailer 10, requires the recorded paths, checks that every exported
lib/<name> has a forward to the same module, and fails without the CommonJS
marker.

Thanks to xet7 !
2026-11-05 21:39:02 +02:00
github-actions[bot] e9c5888485 Bump versions for v12.95 2026-11-05 19:04:13 +00:00
Lauri Ojansivu 09c9d72398 Prepare v12.95 release 2026-11-05 21:03:04 +02:00
Lauri Ojansivu 4f3d45f3de CHANGELOG: the attachment upload policy method is loaded.
Thanks to Raymond-Voogt and xet7 !
2026-11-05 20:57:07 +02:00
Lauri Ojansivu 3aaa46fdae The attachment upload policy method is loaded, so a card hides the "+" when uploads are blocked.
d8b3579117 added server/methods/attachmentUploadPolicy.js but nothing
imported it, so the method did not exist: the card's
Meteor.callAsync('attachmentUploadPolicy') failed, the client treated that
as "not blocked", and the "+" stayed. server/imports.js now imports it with
the other server methods. tests/serverModulesLoaded.test.cjs, which failed
on it, passes.

Thanks to Raymond-Voogt and xet7 !
2026-11-05 20:57:01 +02:00
Lauri Ojansivu 59f1b705e0 CHANGELOG: missing users, organizations and teams return 404, Requested by on the minicard, and the pull request rules.
Thanks to AhmedLukman, mueschel and xet7 !
2026-11-05 20:56:19 +02:00
Lauri Ojansivu 22e5df55d7 Requested by and Assigned by show on the minicard when a member was picked, with the same icon everywhere.
The opened card's + button picks board members into card.requesters /
card.assigners; the free-text requestedBy / assignedBy is optional. The
minicard only drew the free text, so a requester picked the usual way never
showed, although Show on Minicard was on. It now draws the picked members
as avatars beside the typed name, as the opened card does.

Card Settings showed Requested by with a user-plus icon and Assigned by with
a shopping cart, the other way round from the opened card and the minicard.
The settings rows now use the cart for Requested by and user-plus for
Assigned by.

tests/minicardRequestedAssignedBy6872.test.cjs pins the avatars, the
either-or condition and one icon per field across all three places, and
fails on a minicard person block gated on the free text alone.
tests/playwright/specs/minicard-requested-by-6872.e2e.js drives it.

Thanks to mueschel and xet7 !

Fixes #6872.
2026-11-05 20:55:28 +02:00
Lauri Ojansivu 016b4bf3bd CLAUDE.md, AGENTS.md: only maintainer mode commits directly and updates changelogs; one pull request per feature or fix.
A pull request contributor, in wekan/wekan and in the maintainer's
repositories under .tools/, sends each feature or fix as its own pull
request, with only related changes in it, and changes no changelog: the
maintainer writes the entry after merging, when there is a commit to link.
The CHANGELOG section and the security CRITICAL entry say they are
maintainer mode only, and "good practice for everyone" no longer includes
the CHANGELOG structure. tests/changelogFormat.test.cjs checks both files.

Thanks to xet7 !
2026-11-05 20:55:28 +02:00
Lauri Ojansivu 4d44349ab6 Merge pull request #6874 from AhmedLukman/codex/rest-people-features-not-found
Return 404 for missing organization and team feature updates.
2026-11-05 20:53:48 +02:00
Lauri Ojansivu d3edc56170 Merge pull request #6873 from AhmedLukman/codex/user-update-not-found
Return 404 for updates to missing users
2026-11-05 20:53:12 +02:00
Ahmed Lukman 5134e3d2a8 Return 404 for missing organization and team feature updates.
Return a JSON not-found response when the feature update lookup finds no organization or team. Cover missing records, successful updates, authorization, and reactive People-table checkboxes.

Thanks to AhmedLukman and xet7 !
2026-11-05 21:28:38 +03:00
Ahmed Lukman f686b76023 Keep the user update fix limited to code and tests.
Thanks to AhmedLukman and xet7 !
2026-11-05 20:54:17 +03:00
Ahmed Lukman cd2adcad02 Document user update not-found responses.
Thanks to AhmedLukman and xet7 !
2026-11-05 20:48:43 +03:00
Ahmed Lukman 0766a65fef Return 404 for updates to missing users.
Thanks to AhmedLukman and xet7 !
2026-11-05 20:47:06 +03:00
Lauri Ojansivu 58ad81e301 continuousBackup integration suite: stop each engine before its directory is removed.
Three tests registered the hook that removes their temp directory before the hook
that stops their engine. node:test runs after-hooks in the order they were
registered, not last-in first-out, so the directory was gone when stop() sealed
into it, and the ENOENT (files/index.ndjson) surfaced after the test as
"generated asynchronous activity after the test ended" - which failed the whole
suite although all 8 tests passed. One hook now stops the engine first and then
removes the directory. Five runs in a row, and the suite runner, pass.

Thanks to xet7 !
2026-11-04 19:20:58 +02:00
Lauri Ojansivu 2912f4b080 Close #6870: attachments are already off site-wide in Admin Panel / Attachments / Transfer limits.
How: Admin Panel / Attachments / Transfer limits, set "Attachments upload" and
"API upload" to Blocked. That is the site-wide "Disable attachments" the issue
asks for, as two settings instead of one checkbox.

Comment for the issue:

  A site-wide switch for this already exists, so a new "Disable attachments"
  setting is not needed. Open Admin Panel / Attachments / Transfer limits and set
  "Attachments upload" and "API upload" to Blocked. While they are on, the server
  refuses an upload on every path - the upload pipeline, the api.attachment.upload
  and api.attachment.copy methods, and the /api/attachment upload,
  upload-background and copy routes - on every board, including boards created
  later, and no board's allowsAttachments flag has to be touched. Download, move
  and delete are not affected, so files uploaded earlier stay reachable. What was
  missing is that a card still showed the "+" button and accepted a drop for an
  upload that would then be refused; since d8b3579117 the card asks the server
  once a minute and hides both. There is no second setting, so nothing can
  disagree with what the server enforces. Two settings instead of one checkbox
  is the one difference from the request.

Thanks to Raymond-Voogt and xet7 !

Fixes #6870,
2026-11-04 19:17:53 +02:00
Lauri Ojansivu 97d1eca09d CHANGELOG: In short for the upcoming release. 2026-11-04 19:12:14 +02:00
Lauri Ojansivu 95b75177b8 CHANGELOG: continuous backup runs on one server at a time, with a lease. 2026-11-04 19:12:06 +02:00
Lauri Ojansivu 880ec2d707 Continuous backup runs on one server at a time, with a lease.
With several WeKan servers on one database, every server applied the continuous
backup setting, so each of them tailed the oplog and wrote its own archives into
the same target directory (#6861, item 3).

The leader holds a lease: one continuousBackupLease document with its id and an
expiry. Taking it is one atomic operation (an insert while there is none, else
an update that matches only an unheld, own or expired lease), the leader renews
it every 15 seconds and it lapses after 45, so another server takes over within
a minute when the leader stops. A server that loses the lease stops its stream.
Startup, saving the settings and a change saved on another server all converge
through that loop, and a restore or a fetch from the cloud takes the lease and
waits one renewal so it is the only writer. One server is always its own leader.

Checked against the real driver on FerretDB (SQLite): 24 clients at once give
exactly one leader; renewal, expiry, loss of the old leader and release behave.
tests/continuousBackupLease.test.cjs pins the same and the wiring in
server/continuousBackup.js. docs/Backup/Continuous-Backup.md no longer says
single-server only.

Thanks to Raymond-Voogt and xet7 !

Refs #6861, not closed: the remaining items of the issue were fixed in 4e28465964.
2026-11-04 19:11:32 +02:00
Lauri Ojansivu dbc7cccea3 CHANGELOG: a card no longer offers the attachment plus when uploads are blocked for the whole site. 2026-11-04 19:07:11 +02:00
Lauri Ojansivu d8b3579117 A card no longer offers the attachment "+" when uploads are blocked for the whole site.
#6870 asks for a site-wide switch that turns attachments off, because the
per-board allowsAttachments flag has to be set on every board. That switch is
there already: Admin Panel / Attachments / Transfer limits, "Attachments upload"
and "API upload" set to Blocked, and the server refuses on every path - the
upload pipeline, api.attachment.upload and .copy, and the /api/attachment
upload, upload-background and copy routes. What was missing is that the card
still showed the "+" button and accepted a drop for an upload that would be
refused.

attachmentUploadPolicy tells a signed-in client the one boolean (nothing about
the limits), asked at most once a minute, and the "+" and the drop/paste upload
follow it. No second setting is added, so there is nothing that could disagree
with what the server enforces. tests/attachmentUploadPolicy.test.cjs pins the
client, the method, and the server enforcement it relies on, and that no
"disableAttachments" setting exists.

Thanks to Raymond-Voogt and xet7 !

Refs #6870, not closed: the reporter asked for a single Features checkbox, and the
two Blocked limits are two settings.
2026-11-04 19:07:09 +02:00
Lauri Ojansivu c7e53e9f38 CHANGELOG: an integration update for a missing or wrong-board integration returns 404. 2026-11-04 19:04:14 +02:00
Lauri Ojansivu 6012521ccc An integration update for a missing or wrong-board integration returns 404.
PUT /api/boards/:boardId/integrations/:intId answered 200 with the requested id
when no integration matched that id and board (PR #6871). The two activities
routes (POST and DELETE .../activities) had the same fault: they updated
nothing and answered 200 with `data: null`. All three now look the integration
up on that board after board-admin authorization and answer the JSON 404 the GET
route already uses (#6862), before validating or writing anything.

tests/restIntegrationUpdate.test.cjs and the Playwright spec are the PR's.
tests/restIntegrationWriteRoutes404.test.cjs adds the activities routes and
pins, for the whole file, that every handler that updates an integration looks
it up first and only after authorization.

Thanks to AhmedLukman and xet7 !
2026-11-04 19:04:12 +02:00
Lauri Ojansivu 0195a5b3d4 Merge remote-tracking branch 'origin/main' 2026-11-04 15:02:36 +02:00
Lauri Ojansivu b12e761afb CHANGELOG: missing-board exports return 404 and an over-long export token 400. 2026-11-04 14:50:49 +02:00
Lauri Ojansivu 0c0d666ffa Missing-board exports return 404, and an over-long export token returns 400.
The board Excel export answered a missing board with HTTP 200 and the text
"Board not found" (PR #6869). The two PDF export routes had the same fault, and
the Excel and PDF routes answered an over-long auth token with 200 too, while
export.js already used 404 and 400. They now choose their status the same way.

tests/boardExcelNotFound.test.cjs and the Playwright spec are the PR's.
tests/exportErrorStatuses.test.cjs scans every models/export*.js route for a
text answer without a status, so the fault cannot return anywhere there.

Thanks to AhmedLukman and xet7 !
2026-11-04 14:50:33 +02:00
Lauri Ojansivu fc2ad0a48b CHANGELOG: nodemailer, body-parser, qs, uuid and postcss-selector-parser raised in the bundle. 2026-11-04 14:45:57 +02:00
Lauri Ojansivu dad03959d9 Raise nodemailer, body-parser, qs, uuid and postcss-selector-parser in the bundle.
A Grype scan of the image found advisories in packages that Meteor's own
packages carry in programs/server/npm/node_modules/meteor/<package>, which
package.json cannot reach.

nodemailer 8.0.11 -> 10.3.1: meteor/email only calls createTransport, the
'stream' plugin hook, nodemailer/lib/well-known and nodemailer/lib/mail-composer,
and all four work in 10.3.1 (checked with the bundle's own package specifiers,
through its exports map). body-parser 1.20.6/1.20.8 -> 1.20.9 and 2.3.0 ->
2.4.0: a minimum may now be "name@MAJOR", so each major is raised inside
itself. qs -> 6.16.1, postcss-selector-parser 6.1.4 -> 6.1.5.

uuid 8.3.2 and 9.0.1 -> 11.1.1: 11 moved its entry point, which is why it was
refused before. The bumper now writes the recorded dist/index.js back as an
ESM re-export of dist/esm/index.js, which require() loads and which leaves
uuid's own ESM and CJS entry points as they are.

glob 14 and minimatch 11 are already in package.json. Run against a copy of
the published image's bundle, the bumper raises all of these and every raised
copy loads.

Thanks to the Grype scan reporter and xet7 !
2026-11-04 14:45:04 +02:00
github-actions[bot] ac960e7d81 Bump versions for v12.94 2026-11-04 01:07:15 +00:00
Lauri Ojansivu 3213d7fa1d Prepare v12.94 release 2026-11-04 03:06:07 +02:00
Lauri Ojansivu 4757676abf CHANGELOG: on a phone in desktop mode the opened card starts below the header bar. 2026-11-04 03:04:05 +02:00
Lauri Ojansivu efaf94242e On a phone in desktop mode the opened card starts below the header bar.
The header wraps to several rows on a phone, far taller than the 8px the
floating card docks at, so the card's top row sat among the header's rows and
was only seen by dragging. Below 801px a card without an inline top, popup
cards included, starts at --wekan-header-height + 8px; a dragged place is an
inline top and is never overridden.

Thanks to the iPhone 12 mini reporter and xet7 !
2026-11-04 03:03:59 +02:00
Lauri Ojansivu d87baa0244 CHANGELOG: the opened card covers the header bar on an iPad. 2026-11-04 02:56:44 +02:00
Lauri Ojansivu 006d24502c The opened card covers the header bar on an iPad: the header drops to the content's level.
On iPad Chrome and Safari (both WebKit) the opened card's top row - the collapse
caret, the card number, the menu, maximize and X - was behind the header bar,
although the card has z-index 2001 and the header 1000. iOS WebKit makes every
scrolling element (#content, the board wrapper) a stacking context of its own, so
the card's z-index is compared only inside it while the header bar, a flex item of
body and a stacking context too, is painted over all of it. While a card is open
the header bar (#header-quick-access) is now at z-index 0, the level of the
content, which comes after it in the page and so paints over it; with no card open
it keeps 1000.

A browser spec makes #content and the board wrapper stacking contexts with
isolation: isolate, which is the same trap, and shows the card over the header open
and collapsed, its close button reachable, in Chromium and WebKit; without the rule
it fails with the card's top at 8 behind a header 88 high, as in the screenshot.

Thanks to xet7 !
2026-11-04 02:56:33 +02:00
Lauri Ojansivu 8f4efbca57 Merge remote-tracking branch 'origin/main' 2026-11-04 01:35:33 +02:00
Lauri Ojansivu dde09dead5 CHANGELOG: where the time goes when a WeKan board is opened. 2026-11-04 01:21:48 +02:00
Lauri Ojansivu 3621413d6f Speed test: where the time goes when a WeKan board is opened, on FerretDB and on MongoDB.
app/board-open.cjs seeds a board of 200, 2000 or 5000 cards, opens it in Chromium and
records the wall clock to the first card and until the DDP WebSocket is quiet, what
crossed it, the Meteor server's CPU by who spends it (Node inspector) and FerretDB's
CPU (pprof). Two runs on each database: the Meteor server's CPU is the same on both
(0.1-0.5 s) and the DDP server is 1% of it; a 5000-card board takes FerretDB (SQLite)
400-600 ms longer to open, the data sent to the browser being the same ~210 KB, a
small board 70-100 ms. FerretDB uses about 4 s of CPU for the 5000-card open: SQLite
39%, filtering in Go 34%, find/getMore/aggregate 0.7-0.8 s each. The README says what
was measured and what was not: the development build, one machine, the browser's own
time not separated, and the aggregate's source not identified.

Thanks to Raymond-Voogt and xet7 !
2026-11-04 01:21:16 +02:00
github-actions[bot] 3c3fc07b3c Bump versions for v12.93 2026-11-03 23:04:07 +00:00
Lauri Ojansivu c1e8976b28 Prepare v12.93 release 2026-11-04 01:02:51 +02:00
Lauri Ojansivu dcce935310 CHANGELOG: the opened card and popups move under a finger. 2026-11-04 01:00:21 +02:00
Lauri Ojansivu 1b05673200 The opened card and every popup move under a finger: Pointer Events and touch-action.
On an iPad the opened card did not move when dragged by a finger from the collapse
caret, the space beside it, the card number, or the drag handle, while the page
behind it scrolled. The card was moved by mousedown / mousemove / mouseup, and a
finger sends none of them while it moves; the drag areas also lacked
touch-action: none, so the browser took the gesture for a scroll. The card window
is now moved by Pointer Events: a press on the header row or the handle moves it, a
press on a link or button in the row (the caret, close, the menu) waits for a real
drag so a tap still does its job and a drag does not also collapse or close, the
title still edits, and with drag handles on the handle alone moves the window. The
card number is above the title's edit zone so it drags too, the window is kept
within reach, and touch-action: none is on the card header, its handle and every
popup's title bar and handle. Blaze hands over a jQuery event without isPrimary, so
the native event is read.

A browser spec drags with a real touch (DevTools protocol, Pointer Events) from the
caret, the number and the space beside the title, with handles on and off, a tap
that still collapses, a mouse that still drags and a popup; without the fix the
finger tests fail.

Thanks to xet7 !
2026-11-04 01:00:19 +02:00
Lauri Ojansivu dafb63299f CHANGELOG: a popup card covers the header bars. 2026-11-04 00:42:59 +02:00
Lauri Ojansivu 84b0480aa8 An opened card opened as a popup covers the header bars, in every browser.
A card opened as a popup (a search result, a table row, a Roadmap/Gantt bar) got
the generic wide-layout rule top: 97px and z-index 30, because the desktop-mode
rules that dock an opened card at the top (top: 8px, z-index 2001) skip a popup.
So it opened BELOW the header, and collapsed it was a bar floating over the
swimlane, while the same file's popup wrapper rule says it must cover the header
bars. The popup card now docks at top: 8px with z-index 2001 in the wide layout;
mobile mode keeps its own full-window rule. A browser spec shows the card over
the header, open and collapsed, in Chromium and WebKit, and fails without the fix
(top 97 against a header 88 high).

Thanks to xet7 !
2026-11-04 00:42:57 +02:00
Lauri Ojansivu f6fc97e77a The full screen button says Minimize while the page is in full screen.
The same button leaves full screen; its title and name were still Maximize, which
does not say so. The existing Minimize translation is used, so no language file
changes.
2026-11-04 00:29:00 +02:00
Lauri Ojansivu d7105e531d CHANGELOG: a full screen button in the header bar. 2026-11-04 00:27:30 +02:00
Lauri Ojansivu f25957dcf3 A full screen button in the header bar, to the right of the logo.
It puts the whole page in full screen with the Fullscreen API (the unprefixed one
and the webkit-prefixed one) and takes it out again; the icon follows the
browser's own events, so Esc is understood. The label is the existing Maximize
translation, so no language file changes. It is drawn only where the browser
offers the API and the page may use it: not on iPhone, not in an iframe without
allowfullscreen. Unit tests drive the pure part with fake documents, including a
refusal; a browser spec checks the position, the label, entering, leaving, Esc,
and that nothing is drawn where the API is not enabled, in Chromium and WebKit.

Thanks to xet7 !
2026-11-04 00:27:28 +02:00
Lauri Ojansivu 984ea93aa8 CHANGELOG: Dependabot alerts 180 and 183-187, and the SQLite membership test fix. 2026-11-04 00:13:58 +02:00
Lauri Ojansivu 2441563008 Speed test: SQLite after the membership test fix (68.3 ms, 4.1x), the Due Cards profile and what was tried.
Thanks to Raymond-Voogt and xet7 !
2026-11-04 00:13:34 +02:00
Lauri Ojansivu f3950b9adc Dependabot alerts 180 and 183-187: minimatch in the other lockfiles, and the Go modules of the embedded benchmark.
minimatch before 11.0.1 (GHSA-g3hv-75wg-m249): npm-packages/exceljs still had
10.2.6 through readdir-glob 3, and the vendored jade lockfile under
meteor-jade-loader had three copies through glob 13 and test-exclude; both get
an override to minimatch 11, as the root lockfile has. A test now reads every
package-lock.json of the repository, not only the reported ones.

The benchmark module of embedded engines pulled klauspost/compress 1.18.0,
golang.org/x/net 0.23.0 and golang/glog 1.0.0 (GHSA-259r-337f-4rfw,
GHSA-5cv4-jp36-h3mw, GHSA-qxp5-gwg8-xv66, GHSA-vvgc-356p-c3xw,
GHSA-6wxm-mpqj-6jpf). They are raised to 1.18.7, 0.62.0 and 1.2.4, and
govulncheck then found more in x/net, x/text and OpenTelemetry, which are raised
too (x/net 0.62.0, x/text 0.43.0, otel 1.42.0): it now reports none. A test
holds every go.mod of the repository at or above these floors.

Thanks to dependabot and xet7 !
2026-11-04 00:12:45 +02:00
Lauri Ojansivu 5e07b8db6c Docs: the speed test, version history and embedded engines READMEs link directories, not README.md files. 2026-11-03 23:59:03 +02:00
Lauri Ojansivu eeeb0dbec9 CHANGELOG: Go databases compared with FerretDB's SQLite. 2026-11-03 23:47:33 +02:00
Lauri Ojansivu 96d56ecc6c Go databases that could be linked into FerretDB, against its SQLite: none is faster where it counts.
A benchmark of its own (docs/Databases/FerretDB/1/SpeedTest/embedded, a Go module,
no cgo) loads the speed test's cards into SQLite set up as FerretDB sets it and
into Pebble, Badger, bbolt, BuntDB, Nutsdb and Clover, and asks each the six
questions WeKan asks of cards, at 15,377 and 153,768 cards. A board's cards take
the same time on every engine that is not slower, because decoding the JSON
dominates; the key-value stores win single reads and writes by microseconds that
a FerretDB call hides; Pebble is 12x slower on fetching a board, Clover 90-190x
slower in total, and bbolt with its default fsync 100-160x slower on writes. The
README accounts for every entry of the awesome-go section.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 23:47:13 +02:00
Lauri Ojansivu aadfd9b6b3 CHANGELOG: Dolt in the speed test and the version history experiment. 2026-11-03 23:19:00 +02:00
Lauri Ojansivu 1c44876beb Speed test: Dolt, and what a database with version history would give WeKan's undo and redo.
Dolt runs under the existing MySQL handler with no new handler and gives the same
answers, but is by far the slowest: 20,470 ms in total, every card query a
2.8 s whole-table scan. The VersionHistory scripts drive WeKan-shaped changes
through FerretDB onto Dolt (a commit per write, undo and redo with DOLT_REVERT,
AS OF, +0.4 ms a write, a conflict when the same document is restored again) and
through the REST API of TerminusDB (author and message per commit, field-level
restore). ClickHouse and StarRocks were tried with the MySQL handler and need a
handler of their own; the README says where each fails.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 23:18:51 +02:00
Lauri Ojansivu bd86b518ea CHANGELOG and tests: the small-scale speed test, pgrust, TiDB and the databases that cannot run FerretDB. 2026-11-03 22:47:16 +02:00
Lauri Ojansivu f576a0cca9 Speed test: TiDB added, and the README says what was tried: pgrust, TiDB, Noria, YugabyteDB, CockroachDB.
TiDB (MySQL protocol, Apache-2.0) runs FerretDB after its metadata table's
generated columns became VIRTUAL, and gives the same answers as MongoDB; it is
a speed test row and opt-in for the conformance run (WEKAN_CONFORMANCE_TIDB=1).
Noria has no JSON type, triggers or generated columns, which the MySQL handler
needs; YugabyteDB refuses a CREATE INDEX on a jsonb expression, which every
FerretDB v1 PostgreSQL index is; CockroachDB is not open source. The README
also records that analyzing a PostgreSQL table after its indexes are created
took the PostgreSQL row from 454 to 145 ms, and that the same on MySQL and
MariaDB did nothing.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 22:45:56 +02:00
Lauri Ojansivu 46c0ad43e8 Speed test: small-scale results ordered fastest to slowest, with times slower, and pgrust.
Results.md now opens with an At a glance table (rank, total, how many times
slower than the fastest, the same for the slowest user), every table is ordered
by its own numbers, the header says the real data size and the real operating
system, and the load table is ordered by the whole load time. pgrust
(PostgreSQL in Rust, malisper/pgrust:v0.4-beta) is one more row through the
same PostgreSQL handler; in the conformance run it is opt-in with
WEKAN_CONFORMANCE_PGRUST=1. The run is 1/20 of a production WeKan so it takes
minutes.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 22:36:56 +02:00
Lauri Ojansivu 499772e4c2 Speed test README: sorting in PostgreSQL was tried and was not faster.
The ten newest of 30,116 users took 52 ms with an ORDER BY in the database and
38-49 ms decoded and sorted in Go, so the change was not kept; the README says
what was measured so it is not repeated.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 21:53:31 +02:00
Lauri Ojansivu 90fe133363 Merge remote-tracking branch 'origin/main' 2026-11-03 21:37:36 +02:00
Lauri Ojansivu b812d36871 CHANGELOG: the speed test README. 2026-11-03 21:24:06 +02:00
Lauri Ojansivu d05a42a5af Speed test README: the FerretDB v1.100-v1.105 and WeKan improvements since the last run.
SQLite sorts and stores JSONB, PostgreSQL uses a GIN index for membership and
pushes down $or, $elemMatch and $and, MySQL and MariaDB read through their
indexes with multikey side tables, and WeKan stopped scanning whole collections
every hour or at every start. Results.md still shows v1.99.0, which the README
now says, and the open items are updated to match.

Thanks to Raymond-Voogt and xet7 !
2026-11-03 21:24:03 +02:00
github-actions[bot] 42507de1d0 Bump versions for v12.92 2026-11-03 18:51:08 +00:00
Lauri Ojansivu 131b0cc509 Prepare v12.92 release 2026-11-03 20:48:47 +02:00
Lauri Ojansivu 93ae5ac6ee Merge remote-tracking branch 'origin/main' 2026-11-03 20:46:48 +02:00
Lauri Ojansivu 34bb91d1f5 CHANGELOG: a missing REST checklist answers a JSON 404. 2026-11-03 20:43:21 +02:00
Lauri Ojansivu d231df56fc REST API: a missing checklist, or one under another card, answers a JSON 404, not an empty 500.
GET /api/boards/:boardId/cards/:cardId/checklists/:checklistId answered 500 with
no body when the checklist did not exist or belonged to another card. It now
answers 404 with the same error text the checklist update and delete routes
use; board access and the parent-card check are unchanged. Route tests drive
the registered handler (found, missing, other card, other board, denied, lookup
failure) and a browser spec reads the checklist over REST and after a reload.

Thanks to AhmedLukman and xet7 !
2026-11-03 20:43:06 +02:00
github-actions[bot] e422ab691e Bump versions for v12.91 2026-11-03 18:01:08 +00:00
Lauri Ojansivu 3f712a0445 Prepare v12.91 release 2026-11-03 19:59:16 +02:00
Lauri Ojansivu d99dadadee Merge remote-tracking branch 'origin/main' 2026-11-03 19:42:04 +02:00
Lauri Ojansivu a58b7bee2c CHANGELOG: code scanning alert 600, a test replacing getLabel with itself. 2026-11-03 19:17:12 +02:00
Lauri Ojansivu 62da564a1e Code scanning #600: a test no longer replaces getLabel with itself, and the guard knows anchored patterns.
tests/boardLabelLookup.test.cjs sliced the getLabel method out of models/boards.js
and then ran .replace(/^getLabel/, 'getLabel') on it, which does nothing: the
slice already starts at getLabel. tests/noIdentityReplacement.test.cjs skipped
every regular expression with an anchor, so it could not see it; a leading ^ or
a trailing $ over plain characters is now compared like any other.

Thanks to GitHub CodeQL and xet7 !
2026-11-03 19:16:44 +02:00
github-actions[bot] 2ada2e2d61 Bump versions for v12.90 2026-11-03 15:58:18 +00:00
Lauri Ojansivu 5de1acf496 Prepare v12.90 release 2026-11-03 17:57:04 +02:00
Lauri Ojansivu 3008bb5606 Merge remote-tracking branch 'origin/main' 2026-11-03 17:55:41 +02:00
Lauri Ojansivu 4f7521ae3c Tigre: the login page background label uses the Tigre word for color, not the Tigrinya one.
Thanks to Raymond-Voogt and xet7 !
2026-11-03 17:31:40 +02:00
Lauri Ojansivu 1c63049913 CHANGELOG: login page background color and a readable support page. 2026-11-03 17:20:07 +02:00
Lauri Ojansivu 6b27764452 Admin Panel: a login page background colour, and a readable layout for the login text and the support page.
Lay-out has a new Login page background color (loginBackgroundColor). Only a
hex colour is accepted, so the value in the CSS custom property can never carry
CSS; the text under the login logo turns white on a dark colour and black on a
light one. The colour is on body only while the sign-in layout shows. The
support page and the text under the login logo get a centred column with sized,
bold headings and space between paragraphs. The label is translated into every
language file.

Thanks to Raymond-Voogt and xet7 !

Fixes #6866,
2026-11-03 17:19:22 +02:00
Lauri Ojansivu ae49937b44 Tests: the Playwright specs expect the accessibility-audit colours and drop below the last checklist row.
The belize and nephritis buttons, the limegreen header, the red-string and
typed-relation lines are now the darker shades that keep their contrast, and
the checklist drag released on the sortable's own bottom edge.
2026-11-03 16:48:44 +02:00
Lauri Ojansivu 7f27ea1f26 CHANGELOG: Show focus outline, and Escape closes the add-list composer. 2026-11-03 16:04:58 +02:00
Lauri Ojansivu 144777ae6b Member Settings: a per-member switch to hide the focus outline.
Change Settings has Show focus outline, on by default: the blue ring round the
focused control is an accessibility feature (WCAG 2.4.7), and it also shows
after a click on a minicard. A member who turns it off - profile.hideFocusOutline,
a toggle method that takes the signed-in user only - gets a class on body that
hides the outline and its halo for them and nobody else. The label is translated
into every language file (best effort; the low-confidence ones are listed in the
changelog).

Thanks to mimZD and xet7 !

Refs #6755,
2026-11-03 16:04:44 +02:00
Lauri Ojansivu f4b5025a7b Escape closes the inline add-list composer.
The composer opened after a list (or in an empty swimlane) stays open after a
list is created, so several can be added, and only its X closed it. Escape now
closes it at the level of an inlined form, with the same reset as the X, and not
on a click elsewhere.

Thanks to mimZD and xet7 !

Fixes #6865,
2026-11-03 15:39:24 +02:00
github-actions[bot] cdcf8ec39d Bump versions for v12.89 2026-11-03 13:35:18 +00:00
Lauri Ojansivu 739f9ed7a0 Prepare v12.89 release 2026-11-03 15:33:40 +02:00
Lauri Ojansivu f06d082e34 Merge remote-tracking branch 'origin/main' 2026-11-03 15:33:13 +02:00
Lauri Ojansivu 4036b16774 CHANGELOG: shortcuts run once on a non-Latin keyboard. 2026-11-03 15:30:41 +02:00
Lauri Ojansivu 0110e84ce5 Keyboard shortcuts run once on a non-Latin layout.
On a Farsi (or Cyrillic) keyboard the d shortcut opened the Due date popup and
closed it at a glance. keyboard.js re-dispatched every key typed on another
script as its Latin key, a leftover from Mousetrap; hotkeys-js resolves the key
from event.code itself, so the shortcut ran twice and the repeat clicked the
same control again, closing it. The re-dispatch is removed, and a test drives
real key events for Latin, Farsi and Cyrillic at the library.

Thanks to mimZD and xet7 !

Fixes #6840,
2026-11-03 15:30:39 +02:00
552 changed files with 25725 additions and 10340 deletions

No files matched your search

+1 -1
View File
@@ -174,7 +174,7 @@ jobs:
- name: Build and start in NetBSD
if: steps.node.outputs.skip != 'true' && matrix.os == 'netbsd'
uses: vmactions/netbsd-vm@c8a0d7ddb84619a7a8cc4e652efa7950b3fd9f92 # v1.5.2
uses: vmactions/netbsd-vm@5957cf330aef2e6ddc63c604364c99ce2e02b91d # v1.5.3
with:
release: ${{ matrix.release }}
sync: rsync
+27 -5
View File
@@ -2624,7 +2624,12 @@ jobs:
permissions:
contents: write
needs: [prepare, build-amd64, release]
runs-on: macos-14 # macos-14 runners are Apple Silicon (arm64)
# Apple Silicon (arm64). It was `macos-14`, and GitHub RETIRED that image:
# the v12.98 run's build-mac-arm64 was cancelled with "The configuration
# 'macos-14-arm64-us-default' is not supported", so v12.98 shipped no
# mac-arm64 bundle. macos-15 is Apple Silicon too (the same label mac.yml
# already builds on).
runs-on: macos-15
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
@@ -2900,7 +2905,8 @@ jobs:
contents: write
needs: [prepare, build-amd64, release]
# Intel (x86_64) macOS. It was `macos-13`, and GitHub RETIRED that image -
# actions/runner-images publishes macos-14, macos-15 and macos-26 only, and
# actions/runner-images publishes macos-15 and macos-26 only (macos-14 was
# retired later too), and
# macos-13 is gone from the hosted-runner table. A retired label does not
# fail: the job QUEUES FOREVER waiting for a runner that will never come, so
# every release since then had a build-mac-x64 sitting there for hours until
@@ -2908,8 +2914,8 @@ jobs:
# what skipped the charts jobs (they had not started yet).
# No release has ever carried a wekan-<version>-mac-x64.zip because of it.
# Intel macOS is still offered, under new labels: macos-15-intel and
# macos-26-intel (macos-14/15/26 without the suffix are Apple Silicon). This
# is the same label .tools/TSC already builds its Intel Mac on.
# macos-26-intel (macos-15/26 without the suffix are Apple Silicon). This
# is the same label .tools/Secretchronicles/TSC already builds its Intel Mac on.
runs-on: macos-15-intel
timeout-minutes: 60
env:
@@ -5070,13 +5076,29 @@ jobs:
[ -e "$1" ] || return 1
case "$logs_before" in *" $1 "*) [ "$1" -nt "$attempt_marker" ] ;; *) return 0 ;; esac
}
# snapcraft's execution logs. v12.96 s390x: every attempt printed only
# "Could not find repository at path ...", because snapcraft reports
# the LAST exception; the cause before it - "HTTP Error 503: Service
# Unavailable" / "Error: Timeout" creating that repository - was in
# the "Full execution log" alone. Unread, three Launchpad outages
# counted as three WeKan failures and the arch was not published.
snapcraft_log_dir="$HOME/.local/state/snapcraft/log"
infra_failure() { # did THIS attempt fail on Launchpad, not on WeKan?
grep -qsE 'HTTP Error 5[0-9][0-9]|Error: Timeout|Service Unavailable' "$remote_log" && return 0
# The same v12.96 s390x run's last attempt: the repository the
# timed-out requests had half-created refused our push with
# "You do not have permission to create refs/heads/main" - the
# same credentials pushed every other architecture in that run.
grep -qsE 'remote: You do not have permission to create refs/' "$remote_log" && return 0
local log
for log in snapcraft-wekan-*.txt; do
this_attempts_log "$log" || continue
grep -qsE 'RPC failed;|GnuTLS recv error|fatal: early EOF|Gateway error \[IP: ' "$log" && return 0
done
for log in "$snapcraft_log_dir"/*.log; do
this_attempts_log "$log" || continue
grep -qsE 'HTTP Error 5[0-9][0-9]|Error: Timeout|Service Unavailable' "$log" && return 0
done
return 1
}
# Set once an attempt ends with Launchpad reporting the build as
@@ -5099,7 +5121,7 @@ jobs:
# earlier attempts' logs stay in the directory for the diagnostics
# below, but must not be printed again as if they were this one's.
touch "$attempt_marker"
logs_before=" $(ls snapcraft-wekan-*.txt 2>/dev/null | tr '\n' ' ')"
logs_before=" $(ls snapcraft-wekan-*.txt "$snapcraft_log_dir"/*.log 2>/dev/null | tr '\n' ' ')"
if [ "$fresh_next" = 1 ]; then
echo "The previous attempt's Launchpad build finished without a snap; submitting a new build instead of recovering it."
run_remote_build && rc=0 || rc=$?
+1 -1
View File
@@ -32,6 +32,6 @@ jobs:
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run chart-releaser
uses: helm/chart-releaser-action@df287104677278f869ca5480bd260358397cc3f9 # v1.8.0
uses: helm/chart-releaser-action@92c6b97fa9f664492a2592e0d1d80bc6bf378957 # v1.9.0
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+2 -2
View File
@@ -72,8 +72,8 @@ test-results/
# of them - it replaced an entry per clone (/FerretDB/, /node/, /mongo-tools/,
# /mongosh/, /sandstorm/, /projects/, and the WeKan variants), which had to be
# extended by hand for every new one. build.sh's ensure_tool_repo clones what a
# test run needs into here: .tools/FerretDB, .tools/node-patches,
# .tools/mongo-tools-patches. Nothing in here is WeKan's source, and Meteor is
# test run needs into here: .tools/w/FerretDB, .tools/w/node-patches,
# .tools/w/mongo-tools-patches. Nothing in here is WeKan's source, and Meteor is
# told the same by /.tools/ in .meteorignore.
.tools/
+43 -17
View File
@@ -26,7 +26,7 @@ Work out which you are from the git identity (the next section says how), then:
| Who | Where the work goes |
| --- | --- |
| **The maintainer** — `Lauri Ojansivu <x@xet7.org>` | **Directly on `main`.** Never a feature branch, never a topic branch, never a pull request. |
| **A contributor** — anybody else | **A branch in your own fork, then a pull request** for the maintainer to review. Never a commit on `main`. |
| **A contributor** — anybody else | **A branch in your own fork, then one pull request per feature or fix** for the maintainer to review. Never a commit on `main`, and no changelog changes. |
Nothing else is a third option. If a tool, a task runner or an agent harness offers
some other arrangement — a topic branch for the maintainer, a direct push for a
@@ -41,7 +41,9 @@ released section links a commit that release does not contain.
The contributor's half is review. Nobody but the maintainer commits to `main` in
wekan/wekan, so a change from anyone else arrives as a pull request — which is also
the only place it can be discussed before it lands.
the only place it can be discussed before it lands. That is why each pull request
holds one feature or one fix, and leaves the changelogs to the maintainer: see
"Contributor mode" below.
Check which branch you are on before committing:
@@ -143,12 +145,29 @@ git config user.name && git config user.email
already exists, on <https://wekan.fi/sponsors>, and belongs nowhere else. The **publishing / release
steps** below are available to a human maintainer only: an AI stops after
committing locally and never runs a step that pushes or publishes.
**Maintainer mode is the only mode that commits directly and the only mode that
updates changelogs**, in WeKan and in the maintainer's repositories under
`.tools/` alike. It applies to the changes the WeKan maintainer makes, and to the
changes of an agent working as the maintainer according to this file.
- **Contributor mode** — the identity is somebody ELSE, in a fork or a clone of your
own — a person, or an AI that files pull requests on its own account. Then: do
**not** commit to `main` and do **not** run any release/publishing step. Make the
changes on a branch and open a **pull request** for the maintainer to review. The
"commit as Lauri Ojansivu", "commit directly to `main`", and all release
instructions below are **maintainer-only and do not apply to you**.
**not** commit directly to the repository or to `main`, and do **not** run any
release/publishing step. Make the changes on a branch and open a **pull request**
for the maintainer to review. The "commit as Lauri Ojansivu", "commit directly to
`main`", changelog and release instructions below are **maintainer-only and do
not apply to you**.
Every pull request, to wekan/wekan or to a maintainer's repository under `.tools/`,
follows two more rules:
- **One feature or one fix per pull request.** Changes share a pull request only
when they are related to that one feature or fix. Anything unrelated, however
small, goes in a pull request of its own, so each can be reviewed, merged or
declined without holding up the others.
- **No changelog changes.** Do not edit `CHANGELOG-big.md`, `CHANGELOG.md`, or the
`CHANGELOG.md` or `CHANGELOG` of a repository under `.tools/` — not an Upcoming
entry, not TODO Later. Describe the change in the pull request instead. The
maintainer writes the entry after merging, when there is a commit for it to link.
Who that pull request credits follows the attribution rule above, and the two
cases differ:
@@ -171,8 +190,8 @@ The one repository under `.tools/` that is somebody else's is `sandstorm-io/sand
cloned for reference only.
Everything below marked as maintainer-specific (committing directly, the exact commit
author, and the entire "Making a release" / publishing flow) applies only in maintainer
mode. The rest (translation policy, CHANGELOG structure, tests, validating from code) is
author, updating changelogs, and the entire "Making a release" / publishing flow) applies
only in maintainer mode. The rest (translation policy, tests, validating from code) is
good practice for everyone.
## Process: "Fix open issues"
@@ -206,6 +225,9 @@ When asked to fix an open issue (one issue at a time):
Fixes #1234,
```
A contributor instead commits on a branch for one pull request that fixes this
one issue, with no changelog changes (see "Contributor mode" above).
6. **If the bug is already fixed**, still add a commit that CLOSES the issue (a commit
whose message ends `Fixes #1234,`), noting where/when it was fixed.
@@ -357,7 +379,7 @@ directly after the merge.
tool that reads it sees every file twice and reports the bundler's rewritten
code as if it were source. That is not hypothetical — a source-scanning test
did exactly that and failed on generated code nobody can edit.
- `.tools/wekan.fi` — the WeKan website companion repository.
- `.tools/w/wekan.fi` — the WeKan website companion repository.
- `.tools/` — everything that is NOT part of this repository but is needed to
build, test and release it, in ONE directory that `.gitignore` and
`.meteorignore` already exclude (each used to need its own ignore entry at
@@ -371,10 +393,10 @@ directly after the merge.
| Path | Repository | Branch | What it is |
| --- | --- | --- | --- |
| `.tools/FerretDB` | wekan/FerretDB | `main-v1` | the FerretDB v1 fork WeKan ships as its default database |
| `.tools/node-patches` | wekan/node-patches | `main` | patches to upstream Node.js; builds the `node-<platform>` binaries the bundles, the Docker image and the snap embed |
| `.tools/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `mongo-tools-<arch>`, all eight tools as one program |
| `.tools/TSC` | Secretchronicles/TSC | `devel` | the game xet7 also maintains — same author and maintainer mode, its own GNU ChangeLog and release process |
| `.tools/w/FerretDB` | wekan/FerretDB | `main-v1` | the FerretDB v1 fork WeKan ships as its default database |
| `.tools/w/node-patches` | wekan/node-patches | `main` | patches to upstream Node.js; builds the `node-<platform>` binaries the bundles, the Docker image and the snap embed |
| `.tools/w/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `mongo-tools-<arch>`, all eight tools as one program |
| `.tools/Secretchronicles/TSC` | Secretchronicles/TSC | `devel` | the game xet7 also maintains — same author and maintainer mode, its own GNU ChangeLog and release process |
| `.tools/sandstorm` | sandstorm-io/sandstorm | — | upstream Sandstorm, when present — somebody else's project, cloned for reference |
**Unpacked toolchains and caches.** Downloads, not repositories — put there by
@@ -401,12 +423,16 @@ directly after the merge.
of those repositories goes in that repository's own `README.md` or `docs/`,
never in a new instruction file — and if you find one there, remove it rather
than updating it.
- `.tools/FerretDB` specifics — FerretDB `.go` files must contain **no**
- `.tools/w/FerretDB` specifics — FerretDB `.go` files must contain **no**
application-specific names (say "the client" / "a Meteor 3 driver" / a bare
`#NNNN`); its `CHANGELOG.md` may use `wekan/wekan#NNNN`.
### CHANGELOG
- **[maintainer only] Changelogs are updated only in maintainer mode.** This whole
section is for the maintainer and for an agent working as the maintainer, in WeKan
and in the repositories under `.tools/`. A pull request contributor changes no
changelog in any of them (see "Contributor mode" above).
- **Every repository writes its changelog in the format that repository's own file
already uses.** Open its changelog, read the entries above the place you are
adding one, and match them — never import another project's shape into it. The
@@ -808,7 +834,7 @@ have cost a released section its accuracy:
during ordinary development, and the next run takes the next number.
What decides it is the GitHub Release, not the tag: `prepare` pushes the tag early,
so a tag can exist for a release that never published.
- FerretDB: run `./build.sh release-ferretdb` from `.tools/FerretDB` (no
- FerretDB: run `./build.sh release-ferretdb` from `.tools/w/FerretDB` (no
version). It renames `## Upcoming FerretDB release` to the next version with the
correct git-tag link, commits + tags + pushes, then triggers `release-all.yml` (which
in turn triggers `docker.yml` for the multi-arch image). It refuses to re-release an
@@ -817,7 +843,7 @@ have cost a released section its accuracy:
### Security issues
- Fix the vulnerability, add a CRITICAL section to the WeKan CHANGELOG like previous
entries, and update `.tools/wekan.fi/hall-of-fame/index.html` and the vuln-name
entries (maintainer mode only, as with every changelog change), and update `.tools/w/wekan.fi/hall-of-fame/index.html` and the vuln-name
subdirectory `index.html` like previous security issues.
- **Every security fix gets a TEST and a NEGATIVE TEST, and they are written so the
fault cannot exist ANYWHERE in the codebase — not just at the place it was
@@ -916,7 +942,7 @@ have cost a released section its accuracy:
(they are one icon) — the table itself stays percentage-sized with no fixed or
minimum width, so it fills the page at every browser width.
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
stars follow `.tools/w/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
suggested fix, is 3. When that researcher is later able to add a GitHub security
advisory for the same report, the maintainer requests the CVE through the GHSA
+1131
View File
File diff suppressed because it is too large. Load diff
+36 -28
View File
@@ -5,45 +5,53 @@ each one changed and the project status are in
Written by `node releases/changelog-short.mjs` from CHANGELOG-big.md:
make changes there.
# v12.88 2026-11-03 WeKan ® release
# v13.01 2026-11-08 WeKan ® release
**Every board theme** and **every color a member chooses** is
readable: the dark themes' **Card Settings**, popups and sidebars, the header of
eight themes, and a custom accent, text or label color made legible. A padded
**REST label** name no longer creates a second label.
Translations: the strings still in English are written in **33
more languages**, **Sardinian** is Sardinian instead of Italian and **Moroccan
Arabic** is Darija instead of Persian. Dependabot's updates of **mailparser**,
**isomorphic-git**, **pdfjs-dist** and the S3 libraries are checked and kept,
with the S3 client brought to the version the upload library needs.
[All changes in v12.88](CHANGELOG-big.md#v1288-2026-11-03-wekan--release)
[All changes in v13.01](CHANGELOG-big.md#v1301-2026-11-08-wekan--release)
# v12.87 2026-11-03 WeKan ® release
# v13.00 2026-11-07 WeKan ® release
A **REST API** "get one" that finds nothing answers **404**
instead of 200 with an empty body.
**CreateBleed** is fixed: "Only admins can create boards" now
holds on every path that creates a board - the browser console, the REST API,
imports, copies, clones and cards - not only on the Add Board form. The **REST
API documentation** spells the name **WeKan**, so the API pages published on
wekan.fi pass its brand-spelling check again.
[All changes in v12.87](CHANGELOG-big.md#v1287-2026-11-03-wekan--release)
[All changes in v13.00](CHANGELOG-big.md#v1300-2026-11-07-wekan--release)
# v12.86 2026-11-02 WeKan ® release
# v12.99 2026-11-06 WeKan ® release
The daily **notification cleanup** and the **file integrity
audit**
run once for the whole installation, not once per server.
The **mac-arm64** release bundle builds again: its job asked for
GitHub's retired **macos-14** runner and was cancelled in the v12.98 release
run. The **weba** instructions now live in the weba repository itself.
[All changes in v12.86](CHANGELOG-big.md#v1286-2026-11-02-wekan--release)
[All changes in v12.99](CHANGELOG-big.md#v1299-2026-11-06-wekan--release)
# v12.85 2026-11-02 WeKan ® release
# v12.98 2026-11-06 WeKan ® release
The findings of the **accessibility audit** are fixed in the
default theme: header and label **colour contrast**, names that contain the
visible text, named form fields, card section headings that are real buttons,
and landmarks and headings on every page. axe-core now finds no violation on
22 pages. A cleared database connection pool is reported as a lost connection.
**GroupLinkBleed** is fixed: a board admin can no longer skip the
board methods by writing the board directly, to attach an organization or team
they do not belong to or to add a member past the invitation checks.
**Public board CSV and TSV exports** honour the selected columns, and the REST
**card archive, restore and custom-field** endpoints answer an unmatched card
with a JSON 404 instead of 500, and a REST **card update** can set sort 0, turn
overtime off and clear a description. The **notifications drawer** gets its
attachments again, and the developer instructions cover the **weba** Gambas
companion repository.
[All changes in v12.85](CHANGELOG-big.md#v1285-2026-11-02-wekan--release)
[All changes in v12.98](CHANGELOG-big.md#v1298-2026-11-06-wekan--release)
# v12.84 2026-11-02 WeKan ® release
# v12.97 2026-11-06 WeKan ® release
GitHub code scanning's **code-quality** findings are fixed: a
promise formatted as text, undeclared globals, an upload that hung on an
unreadable file, and the lint around them. `npm run lint:promises`
now finds promises that are not awaited or handled.
`build.sh` and the `releases/` scripts now work on **Arch Linux**
and **Omarchy**, and the release builds no longer lose the **OpenBSD arm64
bundle** to a smoke test that left WeKan running, or the **s390x snap** to
Launchpad outages that were counted as build failures.
[All changes in v12.84](CHANGELOG-big.md#v1284-2026-11-02-wekan--release)
[All changes in v12.97](CHANGELOG-big.md#v1297-2026-11-06-wekan--release)
+43 -17
View File
@@ -26,7 +26,7 @@ Work out which you are from the git identity (the next section says how), then:
| Who | Where the work goes |
| --- | --- |
| **The maintainer** — `Lauri Ojansivu <x@xet7.org>` | **Directly on `main`.** Never a feature branch, never a topic branch, never a pull request. |
| **A contributor** — anybody else | **A branch in your own fork, then a pull request** for the maintainer to review. Never a commit on `main`. |
| **A contributor** — anybody else | **A branch in your own fork, then one pull request per feature or fix** for the maintainer to review. Never a commit on `main`, and no changelog changes. |
Nothing else is a third option. If a tool, a task runner or an agent harness offers
some other arrangement — a topic branch for the maintainer, a direct push for a
@@ -41,7 +41,9 @@ released section links a commit that release does not contain.
The contributor's half is review. Nobody but the maintainer commits to `main` in
wekan/wekan, so a change from anyone else arrives as a pull request — which is also
the only place it can be discussed before it lands.
the only place it can be discussed before it lands. That is why each pull request
holds one feature or one fix, and leaves the changelogs to the maintainer: see
"Contributor mode" below.
Check which branch you are on before committing:
@@ -143,12 +145,29 @@ git config user.name && git config user.email
already exists, on <https://wekan.fi/sponsors>, and belongs nowhere else. The **publishing / release
steps** below are available to a human maintainer only: an AI stops after
committing locally and never runs a step that pushes or publishes.
**Maintainer mode is the only mode that commits directly and the only mode that
updates changelogs**, in WeKan and in the maintainer's repositories under
`.tools/` alike. It applies to the changes the WeKan maintainer makes, and to the
changes of an agent working as the maintainer according to this file.
- **Contributor mode** — the identity is somebody ELSE, in a fork or a clone of your
own — a person, or an AI that files pull requests on its own account. Then: do
**not** commit to `main` and do **not** run any release/publishing step. Make the
changes on a branch and open a **pull request** for the maintainer to review. The
"commit as Lauri Ojansivu", "commit directly to `main`", and all release
instructions below are **maintainer-only and do not apply to you**.
**not** commit directly to the repository or to `main`, and do **not** run any
release/publishing step. Make the changes on a branch and open a **pull request**
for the maintainer to review. The "commit as Lauri Ojansivu", "commit directly to
`main`", changelog and release instructions below are **maintainer-only and do
not apply to you**.
Every pull request, to wekan/wekan or to a maintainer's repository under `.tools/`,
follows two more rules:
- **One feature or one fix per pull request.** Changes share a pull request only
when they are related to that one feature or fix. Anything unrelated, however
small, goes in a pull request of its own, so each can be reviewed, merged or
declined without holding up the others.
- **No changelog changes.** Do not edit `CHANGELOG-big.md`, `CHANGELOG.md`, or the
`CHANGELOG.md` or `CHANGELOG` of a repository under `.tools/` — not an Upcoming
entry, not TODO Later. Describe the change in the pull request instead. The
maintainer writes the entry after merging, when there is a commit for it to link.
Who that pull request credits follows the attribution rule above, and the two
cases differ:
@@ -171,8 +190,8 @@ The one repository under `.tools/` that is somebody else's is `sandstorm-io/sand
cloned for reference only.
Everything below marked as maintainer-specific (committing directly, the exact commit
author, and the entire "Making a release" / publishing flow) applies only in maintainer
mode. The rest (translation policy, CHANGELOG structure, tests, validating from code) is
author, updating changelogs, and the entire "Making a release" / publishing flow) applies
only in maintainer mode. The rest (translation policy, tests, validating from code) is
good practice for everyone.
## Process: "Fix open issues"
@@ -206,6 +225,9 @@ When asked to fix an open issue (one issue at a time):
Fixes #1234,
```
A contributor instead commits on a branch for one pull request that fixes this
one issue, with no changelog changes (see "Contributor mode" above).
6. **If the bug is already fixed**, still add a commit that CLOSES the issue (a commit
whose message ends `Fixes #1234,`), noting where/when it was fixed.
@@ -357,7 +379,7 @@ directly after the merge.
tool that reads it sees every file twice and reports the bundler's rewritten
code as if it were source. That is not hypothetical — a source-scanning test
did exactly that and failed on generated code nobody can edit.
- `.tools/wekan.fi` — the WeKan website companion repository.
- `.tools/w/wekan.fi` — the WeKan website companion repository.
- `.tools/` — everything that is NOT part of this repository but is needed to
build, test and release it, in ONE directory that `.gitignore` and
`.meteorignore` already exclude (each used to need its own ignore entry at
@@ -371,10 +393,10 @@ directly after the merge.
| Path | Repository | Branch | What it is |
| --- | --- | --- | --- |
| `.tools/FerretDB` | wekan/FerretDB | `main-v1` | the FerretDB v1 fork WeKan ships as its default database |
| `.tools/node-patches` | wekan/node-patches | `main` | patches to upstream Node.js; builds the `node-<platform>` binaries the bundles, the Docker image and the snap embed |
| `.tools/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `mongo-tools-<arch>`, all eight tools as one program |
| `.tools/TSC` | Secretchronicles/TSC | `devel` | the game xet7 also maintains — same author and maintainer mode, its own GNU ChangeLog and release process |
| `.tools/w/FerretDB` | wekan/FerretDB | `main-v1` | the FerretDB v1 fork WeKan ships as its default database |
| `.tools/w/node-patches` | wekan/node-patches | `main` | patches to upstream Node.js; builds the `node-<platform>` binaries the bundles, the Docker image and the snap embed |
| `.tools/w/mongo-tools-patches` | wekan/mongo-tools-patches | `main` | patches to the MongoDB Database Tools; builds `mongo-tools-<arch>`, all eight tools as one program |
| `.tools/Secretchronicles/TSC` | Secretchronicles/TSC | `devel` | the game xet7 also maintains — same author and maintainer mode, its own GNU ChangeLog and release process |
| `.tools/sandstorm` | sandstorm-io/sandstorm | — | upstream Sandstorm, when present — somebody else's project, cloned for reference |
**Unpacked toolchains and caches.** Downloads, not repositories — put there by
@@ -401,12 +423,16 @@ directly after the merge.
of those repositories goes in that repository's own `README.md` or `docs/`,
never in a new instruction file — and if you find one there, remove it rather
than updating it.
- `.tools/FerretDB` specifics — FerretDB `.go` files must contain **no**
- `.tools/w/FerretDB` specifics — FerretDB `.go` files must contain **no**
application-specific names (say "the client" / "a Meteor 3 driver" / a bare
`#NNNN`); its `CHANGELOG.md` may use `wekan/wekan#NNNN`.
### CHANGELOG
- **[maintainer only] Changelogs are updated only in maintainer mode.** This whole
section is for the maintainer and for an agent working as the maintainer, in WeKan
and in the repositories under `.tools/`. A pull request contributor changes no
changelog in any of them (see "Contributor mode" above).
- **Every repository writes its changelog in the format that repository's own file
already uses.** Open its changelog, read the entries above the place you are
adding one, and match them — never import another project's shape into it. The
@@ -808,7 +834,7 @@ have cost a released section its accuracy:
during ordinary development, and the next run takes the next number.
What decides it is the GitHub Release, not the tag: `prepare` pushes the tag early,
so a tag can exist for a release that never published.
- FerretDB: run `./build.sh release-ferretdb` from `.tools/FerretDB` (no
- FerretDB: run `./build.sh release-ferretdb` from `.tools/w/FerretDB` (no
version). It renames `## Upcoming FerretDB release` to the next version with the
correct git-tag link, commits + tags + pushes, then triggers `release-all.yml` (which
in turn triggers `docker.yml` for the multi-arch image). It refuses to re-release an
@@ -817,7 +843,7 @@ have cost a released section its accuracy:
### Security issues
- Fix the vulnerability, add a CRITICAL section to the WeKan CHANGELOG like previous
entries, and update `.tools/wekan.fi/hall-of-fame/index.html` and the vuln-name
entries (maintainer mode only, as with every changelog change), and update `.tools/w/wekan.fi/hall-of-fame/index.html` and the vuln-name
subdirectory `index.html` like previous security issues.
- **Every security fix gets a TEST and a NEGATIVE TEST, and they are written so the
fault cannot exist ANYWHERE in the codebase — not just at the place it was
@@ -916,7 +942,7 @@ have cost a released section its accuracy:
(they are one icon) — the table itself stays percentage-sized with no fixed or
minimum width, so it fills the page at every browser width.
- **A report that arrives WITHOUT a GHSA is upgraded when its GHSA arrives.** The
stars follow `.tools/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
stars follow `.tools/w/wekan.fi/hall-of-fame/Audit-Stars.md`: a GHSA with a
suggested fix is 5, a report sent to security@wekan.fi or any other way, with a
suggested fix, is 3. When that researcher is later able to add a GitHub security
advisory for the same report, the maintainer requests the CVE through the GHSA
+2 -2
View File
@@ -38,7 +38,7 @@ FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=12.88
ARG VERSION=13.01
ARG DEBIAN_FRONTEND=noninteractive
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
@@ -151,7 +151,7 @@ ENV \
NODE_VERSION=v26.13.0 \
METEOR_RELEASE=METEOR@3.6-rc.1 \
USE_EDGE=false \
NPM_VERSION=12.2.1 \
NPM_VERSION=12.2.2 \
SRC_PATH=./ \
WITH_API=true \
MONGO_OPLOG_URL="" \
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v12.88.0"
appVersion: "v13.01.0"
files:
userUploads:
- README.md
+10 -10
View File
@@ -2068,7 +2068,7 @@ exit /b 0
REM ===========================================================================
:test_all_databases
REM The same thing build.sh's "All databases (sequential)" runs: build the newest
REM FerretDB v1 from .tools\FerretDB (cloning wekan/FerretDB there if it is not
REM FerretDB v1 from .tools\w\FerretDB (cloning wekan/FerretDB there if it is not
REM there, and installing Go and the module dependencies if they are
REM missing), then run the whole FerretDB v1 query catalogue against every
REM database that has a Docker image for THIS CPU - one at a time, because they
@@ -2105,7 +2105,7 @@ REM ===========================================================================
:test_ferretdb
REM All of FerretDB's own tests, one at a time: unit, vet, integration.
REM
REM FerretDB lives in .tools\FerretDB - companion repos are kept in one directory
REM FerretDB lives in .tools\w\FerretDB - companion repos are kept in one directory
REM that .gitignore and .meteorignore already exclude, instead of one ignored
REM subdirectory each at the repo root. It is cloned here when it is not there,
REM the same as build.sh's ensure_tool_repo does, so neither script depends on
@@ -2117,27 +2117,27 @@ if errorlevel 1 (
echo ERROR: bash was not found. It comes with Git for Windows ^(Git Bash^) and with WSL.
goto end
)
if not exist "%REPO%\.tools\FerretDB\build.sh" (
if not exist "%REPO%\.tools\w\FerretDB\build.sh" (
where git >nul 2>&1
if errorlevel 1 (
echo ERROR: git was not found, so .tools\FerretDB cannot be cloned.
echo ERROR: git was not found, so .tools\w\FerretDB cannot be cloned.
echo Install Git for Windows, or clone it by hand:
echo git clone git@github.com:wekan/FerretDB .tools/FerretDB
echo git clone git@github.com:wekan/FerretDB .tools/w/FerretDB
goto end
)
if not exist "%REPO%\.tools" md "%REPO%\.tools"
echo ==^> FerretDB is not in .tools\ yet; cloning wekan/FerretDB
git clone git@github.com:wekan/FerretDB "%REPO%\.tools\FerretDB"
git clone git@github.com:wekan/FerretDB "%REPO%\.tools\w\FerretDB"
if errorlevel 1 (
echo ==^> SSH clone failed ^(no key for github.com?^); trying HTTPS.
git clone https://github.com/wekan/FerretDB "%REPO%\.tools\FerretDB"
git clone https://github.com/wekan/FerretDB "%REPO%\.tools\w\FerretDB"
)
)
if not exist "%REPO%\.tools\FerretDB\build.sh" (
echo ERROR: .tools\FerretDB\build.sh is still missing after cloning.
if not exist "%REPO%\.tools\w\FerretDB\build.sh" (
echo ERROR: .tools\w\FerretDB\build.sh is still missing after cloning.
goto end
)
bash -c "cd .tools/FerretDB && ./build.sh test-all"
bash -c "cd .tools/w/FerretDB && ./build.sh test-all"
goto end
REM ===========================================================================
+60 -21
View File
@@ -1,9 +1,10 @@
#!/bin/bash
if [ -n "${ZSH_VERSION:-}" ]; then exec /bin/bash "$0" "$@"; fi
echo "Recommended for development: Newest Debian or Ubuntu amd64 based distro, directly to SSD disk or dual boot, not VM. Works fast."
echo "Recommended for development: Newest Debian, Ubuntu, Fedora or Arch/Omarchy amd64 based distro, directly to SSD disk or dual boot, not VM. Works fast."
echo "Note1: If you use other locale than en_US.UTF-8 , you need to additionally install en_US.UTF-8"
echo " with 'sudo dpkg-reconfigure locales' , so that MongoDB works correctly."
echo " so that MongoDB works correctly: Debian/Ubuntu 'sudo dpkg-reconfigure locales',"
echo " Arch/Omarchy: uncomment en_US.UTF-8 in /etc/locale.gen and run 'sudo locale-gen'."
echo " You can still use any other locale as your main locale."
echo "Note2: Console output is also logged to the operation-specific path printed below."
echo "Note3: All logs use .tools/log/<operation>/YYYY-MM-DD_HH-MM-SS/."
@@ -139,25 +140,39 @@ if [ -n "$_wekan_node_version" ] && [ -n "$_wekan_node_arch" ] && [ -x "$_wekan_
fi
export PATH
# ensure_tool_repo <name> [git-url] - the path to .tools/<name>, cloning it if it
# is not there yet. Prints the path on stdout; everything else goes to stderr, so
# a caller can do `dir="$(ensure_tool_repo FerretDB)"`.
# ensure_tool_repo <name> [git-url] - the path to .tools/<owner>/<name>, cloning
# it if it is not there yet. Prints the path on stdout; everything else goes to
# stderr, so a caller can do `dir="$(ensure_tool_repo FerretDB)"`.
#
# The companion repositories are grouped by their GitHub owner, the way they are
# cloned: github.com/wekan/* in .tools/w/, github.com/xet7/* in .tools/xet7/ and
# github.com/Secretchronicles/* in .tools/Secretchronicles/. tool_repo_dir says
# which directory that is, so every script finds a repo in the same place.
#
# SSH first, HTTPS second: a maintainer has a key and wants to push, and anyone
# else still gets a working clone. Returns non-zero when neither works, so the
# caller can say what it cannot do rather than cd into nothing.
function tool_repo_dir(){
local owner="$1" name="$2"
[ "$owner" = "wekan" ] && owner="w"
printf '%s\n' "$WEKAN_TOOLS_DIR/$owner/$name"
}
function ensure_tool_repo(){
local name="$1"
local url="${2:-git@github.com:wekan/$name}"
local https_url="${url/git@github.com:/https://github.com/}"
local dir="$WEKAN_TOOLS_DIR/$name"
local owner="${url#*github.com[:/]}"
owner="${owner%%/*}"
local dir
dir="$(tool_repo_dir "$owner" "$name")"
if [ -d "$dir/.git" ]; then
printf '%s\n' "$dir"
return 0
fi
mkdir -p "$WEKAN_TOOLS_DIR" || { echo "ERROR: cannot create $WEKAN_TOOLS_DIR" >&2; return 1; }
echo "==> $name is not in .tools/ yet; cloning $url" >&2
mkdir -p "$(dirname "$dir")" || { echo "ERROR: cannot create $(dirname "$dir")" >&2; return 1; }
echo "==> $name is not in ${dir#"$WEKAN_DIR"/} yet; cloning $url" >&2
if ! git clone "$url" "$dir" >&2; then
echo "==> SSH clone failed (no key for github.com?); trying HTTPS." >&2
if ! git clone "$https_url" "$dir" >&2; then
@@ -605,6 +620,14 @@ function browser_needs_docker(){
if [ "$PLATFORM_OS" = "linux" ] && [ "$PLATFORM_ARCH" = "arm64" ]; then
return 0
fi
# Playwright builds WebKit for Debian/Ubuntu only. On Arch and Omarchy its
# WebKit is linked against other ICU/libjpeg/libwebp versions than the
# rolling release has and does not launch, while Chromium and Firefox do.
# So WebKit alone goes to the Docker image there, when Docker is installed.
if [ "$browser" = webkit ] && [ "$PLATFORM_OS" = "linux" ] && \
[ "$(_et_linux_family)" = arch ] && docker_available; then
return 0
fi
return 1
}
@@ -632,8 +655,14 @@ function docker_exec(){
# Run a Playwright browser project (chromium / firefox / webkit) inside the
# official Playwright container. First arg is the browser; any extra args are
# passed through to `playwright test`. WeKan must already be running on the host
# (default http://127.0.0.1:3000, Meteor's bundled Mongo on 3001); the container
# (default http://localhost:3000, Meteor's bundled Mongo on 3001); the container
# shares the host network so it can reach both.
#
# The browser must use the test server's ROOT_URL origin, http://localhost:3000.
# Meteor-Files sends every upload to ROOT_URL + /cdn/storage/.../__upload, and
# answers no CORS preflight for another origin, so a page opened on
# http://127.0.0.1:3000 could not upload anything: the drawing specs' Save never
# finished. Health checks with curl may still use 127.0.0.1; a browser may not.
function run_playwright_docker(){
local browser="$1"; shift
local reporoot="$WEKAN_DIR"
@@ -649,7 +678,7 @@ function run_playwright_docker(){
pwver="$(node -e "console.log(require('$pwdir/node_modules/@playwright/test/package.json').version)" 2>/dev/null)"
[ -z "$pwver" ] && pwver="1.60.0"
local image="mcr.microsoft.com/playwright:v${pwver}-noble"
local docker_base_url="${WEKAN_BASE_URL:-http://127.0.0.1:3000}"
local docker_base_url="${WEKAN_BASE_URL:-http://localhost:3000}"
local docker_mongo_url="${WEKAN_MONGO_URL:-mongodb://127.0.0.1:3001/meteor}"
local docker_host_proxy_port=""
local docker_network="host"
@@ -798,7 +827,7 @@ function run_node_e2e_docker(){
-e HOME=/repo/.tools/tmp \
-e TMPDIR=/repo/.tools/tmp \
-e NODE_OPTIONS="${NODE_OPTIONS:-}" \
-e WEKAN_BASE_URL="${WEKAN_BASE_URL:-http://127.0.0.1:3000}" \
-e WEKAN_BASE_URL="${WEKAN_BASE_URL:-http://localhost:3000}" \
-e WEKAN_MONGO_URL="${WEKAN_MONGO_URL:-mongodb://127.0.0.1:3001/meteor}" \
-v "$reporoot":/repo -w /repo \
"$image" \
@@ -825,8 +854,17 @@ function install_playwright_browsers(){
browser_needs_docker "$b" || nativeList="$nativeList $b"
done
if [ -n "$nativeList" ]; then
echo "Installing native Playwright browsers:$nativeList (with system deps; may use sudo)."
( cd "$pwdir" && export HOME="$reporoot/.tools" && npx playwright install --with-deps $nativeList )
if [ "$PLATFORM_OS" = "linux" ] && [ "$(_et_linux_family)" != debian ]; then
# `--with-deps` runs apt-get, which only Debian/Ubuntu have. Elsewhere
# install the libraries the browsers load with the native package
# manager, then the browsers themselves without --with-deps.
echo "Installing native Playwright browsers:$nativeList (system deps from the distribution's packages; may use sudo)."
ensure_playwright_system_deps
( cd "$pwdir" && export HOME="$reporoot/.tools" && npx playwright install $nativeList )
else
echo "Installing native Playwright browsers:$nativeList (with system deps; may use sudo)."
( cd "$pwdir" && export HOME="$reporoot/.tools" && npx playwright install --with-deps $nativeList )
fi
else
echo "All browsers are configured to run via Docker; skipping native browser install."
fi
@@ -1786,7 +1824,7 @@ function floating_promises_checks(){
# not: "Install Playwright browsers" (setup, and the browser stages install what
# they need), and "Count tests by category" (a report, not a check).
#
# FerretDB lives in .tools/FerretDB, cloned on demand by ensure_tool_repo (and by
# FerretDB lives in .tools/w/FerretDB, cloned on demand by ensure_tool_repo (and by
# releases/db-conformance.sh, which needs it a stage earlier). .tools/ is ignored
# by git and by Meteor, so a companion repo cannot reach a commit or a rebuild.
function run_everything(){
@@ -1858,7 +1896,7 @@ function run_everything(){
echo
echo "### 4/4 FerretDB tests ########################################################"
# .tools/FerretDB, cloned here if this is the first run on this machine -
# .tools/w/FerretDB, cloned here if this is the first run on this machine -
# stage 3 usually got there first, but EVERYTHING must not depend on the
# order of its own stages.
ferret_dir="$(ensure_tool_repo FerretDB)" || ferret_dir=""
@@ -1868,8 +1906,8 @@ function run_everything(){
GOFLAGS="$FERRET_GOFLAGS" GOMEMLIMIT="$FERRET_GOMEMLIMIT" \
./build.sh test-all ) || ferret_rc=$?
else
echo "ERROR: .tools/FerretDB/build.sh is missing and could not be cloned."
echo " Clone it by hand: git clone git@github.com:wekan/FerretDB .tools/FerretDB"
echo "ERROR: .tools/w/FerretDB/build.sh is missing and could not be cloned."
echo " Clone it by hand: git clone git@github.com:wekan/FerretDB .tools/w/FerretDB"
ferret_rc=1
fi
@@ -2732,8 +2770,8 @@ RELEASE_SCRIPTS=( "Release|Release All: audit, commit, push and build with Actio
"Git and repo|Migrate a MongoDB database to FerretDB (--help first)|releases/migrate-mongodb-to-ferretdb.mjs|Arguments, e.g. --help||"
"Git and repo|Export local Git history to a new Fossil repository|releases/fossil.sh|||fossil-export"
"Git and repo|Open the local Fossil repository UI|releases/fossil-ui.sh|||fossil-ui"
"Server and VM|Enable and start the SSH server|!sudo systemctl enable ssh && sudo systemctl start ssh||linux|ssh-start"
"Server and VM|Disable and stop the SSH server|!sudo systemctl disable ssh && sudo systemctl stop ssh||linux|ssh-stop"
"Server and VM|Enable and start the SSH server|!sudo systemctl enable \"\$(_et_ssh_unit)\" && sudo systemctl start \"\$(_et_ssh_unit)\"||linux|ssh-start"
"Server and VM|Disable and stop the SSH server|!sudo systemctl disable \"\$(_et_ssh_unit)\" && sudo systemctl stop \"\$(_et_ssh_unit)\"||linux|ssh-stop"
"Server and VM|Enable the ufw firewall|!sudo ufw enable||linux|ufw-enable"
"Server and VM|Disable the ufw firewall|!sudo ufw disable||linux|ufw-disable"
"Server and VM|Remove the Multipass VM|!multipass stop --all && multipass delete --all && multipass purge && sudo snap remove multipass||linux|multipass-remove"
@@ -2999,7 +3037,8 @@ for _once in 1; do
sudo apk add --no-cache bash build-base git curl wget p7zip zip unzip file npm
;;
arch)
sudo pacman -Sy --needed --noconfirm base-devel git curl wget p7zip zip unzip file npm
# Arch and Omarchy: no `pacman -Sy` partial upgrade (see _et_pacman).
_et_pacman base-devel git curl wget 7zip zip unzip file npm
;;
fedora)
sudo dnf group install -y development-tools
@@ -3400,7 +3439,7 @@ for _once in 1; do
if [ -n "$ferret_dir" ] && [ -x "$ferret_dir/build.sh" ]; then
( cd "$ferret_dir" && ./build.sh test-all )
else
echo "ERROR: .tools/FerretDB/build.sh is missing and could not be cloned."
echo "ERROR: .tools/w/FerretDB/build.sh is missing and could not be cloned."
fi
;;
"Run EVERYTHING sequentially: the floating-promises guard (seconds), then WeKan's own tests (mocha, the node suites, import regression, node E2E and all three browsers), then the database conformance run for every database with a Docker image for this CPU, then all of FerretDB's own tests (unit, vet, integration) - one stage at a time, all logs in log/<datetime>/")
@@ -112,9 +112,10 @@ template(name="allBoardsMultiSelectionSidebar")
a.sidebar-btn.js-archive-selected-boards
i.fa.fa-archive
span {{_ 'archive-board'}}
a.sidebar-btn.js-duplicate-selected-boards
i.fa.fa-clipboard
span {{_ 'duplicate-board'}}
if boardCreationAllowed
a.sidebar-btn.js-duplicate-selected-boards
i.fa.fa-clipboard
span {{_ 'duplicate-board'}}
a.sidebar-btn.js-export-selected-boards
i.fa.fa-download
span {{_ 'export-selected-boards'}}
+1 -1
View File
@@ -87,7 +87,7 @@ template(name="attachmentGallery")
.attachment-gallery
//- #6796: a small "+" like every other section's, not a full-width box.
if canModifyCard
if canAddAttachment
a.card-label.add-label.add-attachment.js-add-attachment(title="{{_ 'add-attachment'}}" aria-label="{{_ 'add-attachment'}}")
i.fa.fa-plus
+8
View File
@@ -16,6 +16,7 @@ import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManag
import Attachments from '/models/attachments';
import { Utils } from '/client/lib/utils';
import { buildAttachmentUploadConfig } from '/client/lib/attachmentUploadConfig';
import { attachmentUploadsBlocked } from '/client/lib/attachmentUploadPolicy';
const { cleanFileName } = require('/imports/lib/fileNameDisplay');
const { attachmentTileCompact } = require('/models/lib/attachmentTile.cjs');
import { formatDateTime } from '/imports/lib/dateUtils';
@@ -467,6 +468,10 @@ Template.attachmentGallery.onCreated(function () {
});
Template.attachmentGallery.helpers({
// #6870: the "+" button - a card the user may edit, unless uploads are blocked site-wide
canAddAttachment() {
return Utils.canModifyCard() && !attachmentUploadsBlocked();
},
// #6796: the small file button instead of the preview tile
// (models/lib/attachmentTile.cjs).
attachmentTileCompact(file) {
@@ -672,6 +677,9 @@ export async function handleFileUpload(card, files) {
return [];
}
// #6870: the administrator blocked uploads for the whole site; the server refuses them too
if (attachmentUploadsBlocked()) return [];
// Check if board allows attachments
const board = card.board();
if (!board || !board.allowsAttachments) {
+48
View File
@@ -185,6 +185,23 @@ body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-colla
bottom: 8px;
}
/* A phone in desktop mode: the header bar wraps to several rows (up to five with
the board controls open), far taller than the 8px the floating window docks
at, so the card's own top row (caret, number, menu, X) sat among the header's
rows and was only seen by dragging the card down. Below 801px the card starts
BELOW the header, at the height Utils publishes as --wekan-header-height.
Only a card WITHOUT an inline top matches (`:not([style*="top"])`), so the
place a finger dragged it to is an inline top and is kept when the finger is
lifted - this rule never competes with it. */
@media screen and (max-width: 800px) {
body.desktop-mode .card-details:not(.card-details-popup):not([style*="top"]),
/* a card opened as a popup (search result, table, Roadmap bar) docks the same way */
body.desktop-mode .card-details.card-details-popup:not([style*="top"]),
body.desktop-mode .card-details.card-details-maximized:not(.card-details-popup):not(.card-details-collapsed) {
top: calc(var(--wekan-header-height, 0px) + 8px) !important;
}
}
/* #4822: the MAXIMIZED card must be anchored to the VIEWPORT (position: fixed
with viewport insets), never laid out inside — or measured from — the
scrolled swimlane/board container. Historically the maximized pane was an
@@ -250,6 +267,24 @@ body.desktop-mode .card-details.card-details-collapsed {
color: #b3b3b3;
display: inline-block;
margin-inline-end: 6px;
/* Above the title's edit zone (z-index 1, over the whole title): a press on the
card number moves the window like the rest of the header row; only the title
itself edits. */
position: relative;
z-index: 2;
}
/* The areas that move the window by a finger. Without touch-action: none the browser
takes a finger drag for a scroll and cancels the pointer, so the window never moves
(the page behind it scrolls instead). Only in desktop mode, where the card is a
window; the full-screen card of mobile mode is not moved. */
body.desktop-mode .card-details .card-details-header,
body.desktop-mode .card-details .card-details-header .card-drag-handle {
touch-action: none;
}
body.card-window-dragging {
user-select: none;
-webkit-user-select: none;
}
/* Collapse toggle triangle */
@@ -536,6 +571,19 @@ body.desktop-mode .card-details.card-details-collapsed {
resize: both;
}
/* A card opened as a POPUP (Session popupCardId: from a search result, a table
or a Roadmap/Gantt bar) docks at the very top of the window like every other
opened card, and covers the header bars, as the .pop-over[data-popup=
"cardDetailsPopup"] rule near the end of this file says it must. The 97px
above is the room the header takes for a card that is NOT docked by the
desktop-mode rules (those skip popups), and it opened the popup BELOW the
header: collapsed, it was a bar floating over the swimlane. Mobile mode keeps
its own full-window rule (top: 0 !important). */
.card-details.card-details-popup {
top: 8px;
z-index: 2001;
}
/* Override for mobile mode even on larger screens */
body.mobile-mode .card-details {
width: 100vw !important;
+5 -4
View File
@@ -1270,10 +1270,11 @@ template(name="cardDetailsActionsPopup")
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
unless canModifyCard
li
a.js-create-board-from-card
i.fa.fa-external-link
| {{_ 'linkCardToNewBoard'}}
if boardCreationAllowed
li
a.js-create-board-from-card
i.fa.fa-external-link
| {{_ 'linkCardToNewBoard'}}
span.copied-tooltip {{_ 'copied'}}
//- #1173: the card export popup IS the shared export body - the same checkbox
+85 -79
View File
@@ -81,6 +81,7 @@ const editingLocationId = new ReactiveVar(null);
// Shared with Admin Panel, which links the places accounts log in from the same
// way - models/lib/mapLink.js.
const { mapLinkFor, GCJ } = require('/models/lib/mapLink');
const { cardHeaderPress, dragThreshold, passedThreshold, clampWindow } = require('/models/lib/windowDrag');
@@ -1192,6 +1193,66 @@ Template.cardSectionHeader.events({
},
});
// Moves the card window under a finger or a mouse. `press` is cardHeaderPress's answer.
// A press on a control waits until the pointer has really moved, so a tap still
// collapses, closes or opens the menu; once it moved, the click that follows the drag
// is swallowed.
function beginCardWindowDrag(jqEvent, press) {
// Blaze hands over a jQuery event, which does not copy `isPrimary`; the browser's
// own event has it.
const event = jqEvent.originalEvent || jqEvent;
if (!press.start || !event.isPrimary) return;
if (event.pointerType === 'mouse' && event.button !== 0) return;
const $card = $(event.target).closest('.card-details');
if (!$card.length) return;
const startX = event.clientX;
const startY = event.clientY;
const offset = $card.offset();
const startLeft = offset.left - window.scrollX;
const startTop = offset.top - window.scrollY;
const threshold = dragThreshold(event.pointerType, press.deferred);
const pointerId = event.pointerId;
let moving = false;
const onMove = (e) => {
if (e.pointerId !== pointerId) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (!moving) {
if (!passedThreshold(dx, dy, threshold)) return;
moving = true;
// #6465: where the user put it wins over where the minicard is, so a later
// resize re-places every other window but only clamps this one into view.
markCardDetailsUserMoved($card);
document.body.classList.add('card-window-dragging');
}
e.preventDefault();
const placed = clampWindow(startLeft + dx, startTop + dy, $card.outerWidth(),
{ width: window.innerWidth, height: window.innerHeight });
$card.css({ left: `${placed.left}px`, top: `${placed.top}px` });
};
const onEnd = (e) => {
if (e.pointerId !== pointerId) return;
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onEnd);
document.removeEventListener('pointercancel', onEnd);
document.body.classList.remove('card-window-dragging');
if (moving && press.deferred) {
// The drag started on a link or a button: its click is not a tap.
const swallow = (click) => { click.preventDefault(); click.stopPropagation(); };
document.addEventListener('click', swallow, { capture: true, once: true });
setTimeout(() => document.removeEventListener('click', swallow, { capture: true }), 400);
}
};
document.addEventListener('pointermove', onMove, { passive: false });
document.addEventListener('pointerup', onEnd);
document.addEventListener('pointercancel', onEnd);
}
Template.cardDetails.events({
// #1591 folded each FIELD by its own title, storing a class per card in the
// profile. The section carets replaced it: one handle per section, on the
@@ -1221,86 +1282,31 @@ Template.cardDetails.events({
Users.setPublicCardCollapsed(!currentState);
}
},
'mousedown .js-card-drag-handle'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
// #6465: where the user put it wins over where the minicard is, so a later
// resize re-places every other window but only clamps this one into view.
markCardDetailsUserMoved($card);
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
// The opened card's window is moved by a finger as well as a mouse, so these are
// Pointer Events (a finger sends no mouse events while it moves, which left the card
// fixed on an iPad while the page behind it scrolled) and the areas that start a
// drag are `touch-action: none` (cardDetails.css). With drag handles ON the handle
// is the only thing that moves the window; with them off the header row moves it,
// except the title (it edits). A link or button in the row - the collapse caret, the
// menu, the card number's neighbours - still does its job when tapped, and moves the
// window when dragged. models/lib/windowDrag.js
'pointerdown .js-card-drag-handle'(event) {
beginCardWindowDrag(event, { start: true, deferred: false });
},
'mousedown .js-card-title-drag-handle'(event) {
// The title bar drags the window. Don't interfere with text selection.
if (
$(event.target).closest(
'a, input, textarea, button, select, option, .js-card-details-title',
).length > 0
) {
// #6641: while the editor replaces the header, a textarea press still
// bubbles to this drag bar. Leave it to the browser so mouse dragging
// selects text instead of moving the card window.
return;
}
// #6839: ...and never from the title, which edits - all of it
// (cardDetails.css). The window is moved from the empty space of the
// header row above it, beside the collapse caret and the menu.
if ($(event.target).closest('.card-details-title-edit-zone').length > 0) {
return;
}
// The drag handle has its own handler; without this both would run and the
// window would be moved twice by one press. And when there IS a handle -
// drag handles on - it is the only thing that moves the window, which is
// what turning handles on means.
if ($(event.target).closest('.js-card-drag-handle').length > 0
|| $(event.target).closest('.card-details').find('.js-card-drag-handle').length > 0) {
return;
}
event.preventDefault();
const $card = $(event.target).closest('.card-details');
// #6465: same as the drag handle above — a dragged window keeps its place.
markCardDetailsUserMoved($card);
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
'pointerdown .js-card-title-drag-handle'(event) {
const target = event.target;
const closest = selector => (target.closest ? target.closest(selector) : null);
const press = cardHeaderPress({
onEditable: !!closest('input, textarea, select, option, .js-card-details-title'),
onTitleEditZone: !!closest('.card-details-title-edit-zone'),
// The handle has its own handler above; this one steps aside for it.
onHandle: false,
cardHasHandle: !!closest('.js-card-drag-handle')
|| !!(closest('.card-details') && closest('.card-details').querySelector('.js-card-drag-handle')),
onControl: !!closest('a, button'),
});
if (closest('.js-card-drag-handle')) return;
beginCardWindowDrag(event, press);
},
'click .js-close-card-details'(event) {
event.preventDefault();
+13 -4
View File
@@ -419,18 +419,27 @@ template(name="minicard")
.minicard-location
i.fa.fa-map-marker
| {{#if name}}{{name}}{{else}}{{address}}{{/if}}
//- #6872: the + button on the opened card picks board members into
requesters / assigners; the free-text name is optional. Show both,
as the opened card does, or a picked member never reached here.
if $eq field "requestedBy"
unless $eq currentBoard.allowsRequestedByOnMinicard false
if getRequestedBy
if hasRequestedByOnMinicard
.minicard-requested-by(title="{{_ 'requested-by'}}")
i.fa.fa-shopping-cart
| {{ getRequestedBy }}
each userId in getRequesters
+userAvatar(userId=userId noRemove=true)
if getRequestedBy
span {{ getRequestedBy }}
if $eq field "assignedBy"
unless $eq currentBoard.allowsAssignedByOnMinicard false
if getAssignedBy
if hasAssignedByOnMinicard
.minicard-assigned-by(title="{{_ 'assigned-by'}}")
i.fa.fa-user-plus
| {{ getAssignedBy }}
each userId in getAssigners
+userAvatar(userId=userId noRemove=true)
if getAssignedBy
span {{ getAssignedBy }}
if $eq field "flowtime"
if currentBoard.allowsFlowtimeOnMinicard
if isFlowActive
+7
View File
@@ -261,6 +261,13 @@ Template.minicard.helpers({
isWatching() {
return this.getRealCard().findWatcher(Meteor.userId());
},
// #6872: a picked member (requesters / assigners) or a free-text name.
hasRequestedByOnMinicard() {
return !!this.getRequestedBy() || (this.getRequesters() || []).length > 0;
},
hasAssignedByOnMinicard() {
return !!this.getAssignedBy() || (this.getAssigners() || []).length > 0;
},
showMembers() {
const board = minicardBoardOf(this);
+12
View File
@@ -20,6 +20,18 @@
background: #2573a7;
z-index: 17;
}
/* An opened card covers the header bars, in every browser. iOS WebKit - iPad Safari and
iPad Chrome, which is WebKit too - makes every SCROLLING element (#content, the board
wrapper) a stacking context of its own. The card is inside them, so its z-index (2001)
is only compared there, while the header bar (#header-quick-access, z-index 1000, a
flex item of body and so a stacking context itself) is painted over all of it: the
card's top row - the collapse caret, the card number, the menu, maximize and X - sat
BEHIND the header bar. While a card is open the header drops to the level of the
content, which comes after it in the page and so paints over it; a browser without that
trap shows no difference. */
body:has(.card-details) #header-quick-access {
z-index: 0;
}
#header #header-main-bar {
/* Flex row (was float-based) so the right button group — which ends with the
sidebar hamburger — is always pinned to the right edge at every window width,
+13
View File
@@ -69,6 +69,19 @@ template(name="header")
div.header-logo
img(src="{{pathFor '/logo-header.png'}}" alt="{{headerLogoAlt}}" title="{{headerLogoAlt}}")
//- Full screen, to the right of the logo (iPad Safari, desktop and Android
browsers). Only drawn where the browser has the Fullscreen API and the page
may use it: not on iPhone, not in an iframe without allowfullscreen. The
label is the existing "Maximize" one. It stays before the first
.js-header-collapsible-icon, so "hide the icons" does not hide it.
models/lib/fullscreen.js
if fullscreenSupported
a.board-header-btn.js-toggle-fullscreen(href="#" role="button" title="{{#if isFullscreen}}{{_ 'minimize-window'}}{{else}}{{_ 'maximize-window'}}{{/if}}" aria-label="{{#if isFullscreen}}{{_ 'minimize-window'}}{{else}}{{_ 'maximize-window'}}{{/if}}" aria-pressed="{{#if isFullscreen}}true{{else}}false{{/if}}")
if isFullscreen
i.fa.fa-compress
else
i.fa.fa-expand
// Mobile/desktop mode toggle (swapped with zoom)
// #6680 follow-up: .js-header-collapsible-icon marks every icon the new
// .js-toggle-header-icons-collapsed button (beside Home) hides as a
+32
View File
@@ -59,6 +59,7 @@ function customPageTitle(routeName) {
// Drag-to-scroll on the two top header bars (they are not scroll containers
// themselves, so the drag is forwarded to the board canvas / page scroller).
import '/client/lib/headerDragscroll';
const fullscreen = require('/models/lib/fullscreen');
Meteor.subscribe('user-admin');
Meteor.subscribe('boards');
@@ -70,6 +71,10 @@ Meteor.subscribe('setting');
Meteor.subscribe('announcements');
Template.header.onCreated(function () {
const templateInstance = this;
// Full screen: whether the browser offers it here, and whether the page is in it.
// Both are set when the header is drawn (onRendered), where there is a document.
templateInstance.fullscreenSupported = new ReactiveVar(false);
templateInstance.isFullscreen = new ReactiveVar(false);
templateInstance.currentSetting = new ReactiveVar();
templateInstance.isLoading = new ReactiveVar(false);
templateInstance.subscribe('setting');
@@ -83,6 +88,14 @@ Template.header.onCreated(function () {
});
Template.header.onRendered(function () {
// The Fullscreen API, where it exists and the page may use it (models/lib/fullscreen.js).
// The state follows the browser's own events, so leaving with Esc, or with the
// browser's own control, changes the icon too.
this.fullscreenSupported.set(fullscreen.isSupported(document));
this.isFullscreen.set(fullscreen.isFullscreen(document));
this.onFullscreenChange = () => this.isFullscreen.set(fullscreen.isFullscreen(document));
fullscreen.CHANGE_EVENTS.forEach(name => document.addEventListener(name, this.onFullscreenChange));
this.autorun(() => {
Meteor.userId();
FlowRouter.watchPathChange();
@@ -92,7 +105,19 @@ Template.header.onRendered(function () {
});
});
Template.header.onDestroyed(function () {
if (this.onFullscreenChange) {
fullscreen.CHANGE_EVENTS.forEach(name => document.removeEventListener(name, this.onFullscreenChange));
}
});
Template.header.helpers({
fullscreenSupported() {
return Template.instance().fullscreenSupported.get();
},
isFullscreen() {
return Template.instance().isFullscreen.get();
},
logoSettingsReady() {
return Template.instance().subscriptionsReady();
},
@@ -365,6 +390,13 @@ Template.header.helpers({
});
Template.header.events({
// Full screen on and off. The request is made inside this click, which is the
// user gesture browsers require; a refusal is not an error (models/lib/fullscreen.js).
'click .js-toggle-fullscreen'(event) {
event.preventDefault();
fullscreen.toggle(document);
},
// The starred boards, by name. They were listed inline in this bar; the bar
// is one row now and the names are in the popup. docs/Features/Header-Bar/Header.md
// Titled "Starred Boards", from the key the app already has for that phrase,
+90
View File
@@ -49,6 +49,20 @@ summary:focus-visible,
box-shadow: 0 0 0 4px rgba(21, 101, 192, 0.35);
border-radius: 2px;
}
/* #6755: a member who turned off "Show focus outline" (Member Settings / Change
* Settings) sees no ring: the outline and its halo, for keyboard focus and for the
* fallback below. Field borders and the colour a button takes on focus are not the
* ring and stay. The ring is an accessibility feature, so it is on unless hidden. */
body.has-hide-focus-outline :focus-visible,
body.has-hide-focus-outline a:focus,
body.has-hide-focus-outline button:focus,
body.has-hide-focus-outline [tabindex]:focus {
outline: none !important;
outline-offset: 0 !important;
}
body.has-hide-focus-outline :is(a, button, input, select, textarea, summary, [tabindex], [role="button"], [contenteditable="true"]):focus-visible {
box-shadow: none !important;
}
/* Browsers that do not support :focus-visible fall back to :focus so keyboard
* users are never left without an indicator. */
.no-focus-visible-support a:focus,
@@ -711,6 +725,11 @@ a:not(.disabled).is-active i.fa {
.viewer code {
padding: 10px 12px 7px;
background: #dedede;
/* Its own text colour with its own light background: a dark theme's light
text (#bbb in the dark theme's opened card) was inherited onto #dedede,
1.42:1 (46-axe-theme-contrast). #222 on #dedede is 11.8:1, and
codeHighlight.css's token colours are chosen for this background too. */
color: #222;
overflow-y: auto;
}
.viewer a {
@@ -1133,6 +1152,77 @@ a:not(.disabled).is-active i.fa {
align-items: center;
justify-content: center;
}
/* The administrator's login page background colour (layouts.js sets the two
custom properties and the class while the sign-in layout is shown). The
foreground is white on a dark colour and black on a light one. */
body.has-login-background {
background: var(--login-background);
}
body.has-login-background .textBelowCustomLoginLogo,
body.has-login-background .textBelowCustomLoginLogo a {
color: var(--login-foreground);
}
body.has-login-background hr {
border-color: var(--login-foreground);
opacity: 0.4;
}
/* The text under the login logo: one column, real headings, space between
paragraphs. It has no card or background of its own, so the page colour shows
through. */
.textBelowCustomLoginLogo > * {
width: 100%;
max-width: 860px;
box-sizing: border-box;
padding: 0 16px;
}
.textBelowCustomLoginLogo h1,
.textBelowCustomLoginLogo h2,
.textBelowCustomLoginLogo h3,
.support-page-content h1,
.support-page-content h2,
.support-page-content h3 {
font-weight: 700;
line-height: 1.25;
margin: 1em 0 0.5em;
}
.textBelowCustomLoginLogo h1,
.support-page-content h1 { font-size: 1.8em; }
.textBelowCustomLoginLogo h2,
.support-page-content h2 { font-size: 1.45em; }
.textBelowCustomLoginLogo h3,
.support-page-content h3 { font-size: 1.2em; }
.textBelowCustomLoginLogo p,
.textBelowCustomLoginLogo ul,
.textBelowCustomLoginLogo ol,
.support-page-content p,
.support-page-content ul,
.support-page-content ol {
margin: 0 0 1em;
}
/* The support page: a centred column instead of text flush against the window's
left edge. */
.support-page {
padding: 24px 16px;
}
.support-page-content {
box-sizing: border-box;
max-width: 860px;
margin: 0 auto;
padding: 24px 28px;
background: #fff;
color: #1a1a1a;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
overflow-wrap: anywhere;
}
@media (max-width: 600px) {
.support-page {
padding: 8px;
}
.support-page-content {
padding: 16px;
}
}
.auth-layout .auth-dialog {
margin: 0 !important;
}
+29
View File
@@ -2,6 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
const { OAUTH_PROVIDERS, providerByKey } = require('/models/lib/oauthProviders');
const { loginOriginMismatch, offersProviderLogin } = require('/models/lib/loginOriginMismatch');
const { loginBackgroundStyle } = require('/models/lib/loginBackground');
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { Accounts } from 'meteor/accounts-base';
import Users from '/models/users';
@@ -672,6 +673,34 @@ Template.userFormsLayout.onDestroyed(function () {
this.removeAuthenticationSubmit?.();
});
// The administrator's login page background colour (Admin Panel -> Lay-out).
// It is put on <body> only while the sign-in layout is shown, as two custom
// properties and a class, and taken off again when it closes so the pages after
// sign-in keep their own background. loginBackgroundStyle accepts a hex colour
// only, so the value can never carry CSS of its own.
const LOGIN_BACKGROUND_PROPERTIES = ['--login-background', '--login-foreground'];
function clearLoginBackground() {
document.body.classList.remove('has-login-background');
LOGIN_BACKGROUND_PROPERTIES.forEach(name => document.body.style.removeProperty(name));
}
Template.userFormsLayout.onRendered(function () {
this.autorun(() => {
const setting = this.currentSetting.get();
const style = loginBackgroundStyle(setting && setting.loginBackgroundColor);
if (!style) {
clearLoginBackground();
return;
}
document.body.style.setProperty('--login-background', style.background);
document.body.style.setProperty('--login-foreground', style.foreground);
document.body.classList.add('has-login-background');
});
});
Template.userFormsLayout.onDestroyed(clearLoginBackground);
// The form's title ("Sign In", "Create an Account") is an h3 of the useraccounts
// package and the first heading of the page: a level-one heading, whatever the
// package draws it as (WCAG 1.3.1; axe page-has-heading-one and heading-order).
+5
View File
@@ -39,6 +39,10 @@
first row of a popup, and it must match the space beside it: `.content` has 18px of
side padding, so 6 + 12 = 18 makes the top gap and the left gutter the same. */
.pop-over .header {
/* A popup is moved by its title bar (popup.js, Pointer Events). Without this a
finger drag on it is taken for a scroll and the pointer is cancelled, so only a
mouse could move it. */
touch-action: none;
height: calc(41px * var(--wekan-ui-font-scale, 1));
position: relative;
margin-bottom: 6px;
@@ -1441,6 +1445,7 @@ body.mobile-mode .pop-over .content-wrapper {
button (popup.js MOVABLE_POPUPS / MAXIMIZABLE_POPUPS), placed like the opened
card's: the handle before the title, the button beside the close button. */
.pop-over .header .popup-drag-handle {
touch-action: none;
float: inline-start;
padding: 12px 0 12px 12px;
cursor: move;
+20 -10
View File
@@ -123,8 +123,16 @@ Popup.template.events({
const drag = tpl._dateMove;
if (!drag || drag.pointerId !== evt.pointerId) return;
evt.preventDefault(); evt.stopPropagation();
const boxLeft = Math.max(12, Math.min(window.innerWidth - drag.width - 12, drag.left + drag.dx + evt.clientX - drag.x));
const boxTop = Math.max(12, Math.min(window.innerHeight - drag.height - 12, drag.top + drag.dy + evt.clientY - drag.y));
// The popup's size NOW, not when the drag began: a long menu gets its
// 960px column layout a frame after it is drawn (popupMenuLayout.js), so a
// drag that starts in that frame would keep clamping for the narrow size
// and push the grown popup most of the way out of the window - and undo
// the ResizeObserver that had just put it back inside.
const size = drag.element.getBoundingClientRect();
const width = size.width || drag.width;
const height = size.height || drag.height;
const boxLeft = Math.max(12, Math.min(window.innerWidth - width - 12, drag.left + drag.dx + evt.clientX - drag.x));
const boxTop = Math.max(12, Math.min(window.innerHeight - height - 12, drag.top + drag.dy + evt.clientY - drag.y));
const left = boxLeft - drag.dx;
const top = boxTop - drag.dy;
// Dragging uses physical pointer coordinates in either text direction.
@@ -230,13 +238,16 @@ Popup.template.onRendered(function () {
this._focusTrap = event => trapTabKey(event, this._popupElement);
this._popupElement?.addEventListener('keydown', this._focusTrap);
if (this._popupElement) {
const update = () => {
this._menuLayoutFrame = null;
updatePopupMenuColumns(this._popupElement);
};
this._menuLayoutObserver = new MutationObserver(() => {
if (this._menuLayoutFrame == null) this._menuLayoutFrame = requestAnimationFrame(update);
});
const update = () => updatePopupMenuColumns(this._popupElement);
// At once, not on the next animation frame: a MutationObserver callback
// already runs before the browser paints, and a frame later the popup had
// been drawn - and could be clicked or dragged - at its narrow size, then
// jumped to the 960px column layout under the pointer. A click aimed at
// Board View's "Lists" landed on "Calendar"'s place, and a drag clamped
// for the narrow size (06-views-layout, card-title-edit-and-move-6839).
// update() toggles a class only when it changes, so the attribute it
// observes settles after one more call.
this._menuLayoutObserver = new MutationObserver(update);
this._menuLayoutObserver.observe(this._popupElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
update();
// A movable popup that grows - Drawing replacing the card's menu in this
@@ -278,6 +289,5 @@ Popup.template.onRendered(function () {
Popup.template.onDestroyed(function () {
this._menuLayoutObserver?.disconnect();
this._boundsObserver?.disconnect();
if (this._menuLayoutFrame != null) cancelAnimationFrame(this._menuLayoutFrame);
this._popupElement?.removeEventListener('keydown', this._focusTrap);
});
+11
View File
@@ -38,6 +38,17 @@ Meteor.startup(() => {
}
}
// #6755: a member who hid the focus outline (Member Settings / Change Settings):
// a class on <body>, which layouts.css turns into no ring.
Tracker.autorun(() => {
const user = Meteor.user();
try {
document.body.classList.toggle('has-hide-focus-outline', !!(user && user.profile && user.profile.hideFocusOutline === true));
} catch (_) {
// document.body may not exist yet in exotic embeddings; ignore.
}
});
Tracker.autorun(() => {
const user = Meteor.user();
const profile = (user && user.profile) || {};
@@ -630,6 +630,10 @@ template(name='tableVisibilityModeSettings')
.title {{_ 'text-below-custom-login-logo'}}
.form-group
textarea#text-below-custom-login-logo.wekan-form-control(data-editor-submit=".js-visibility-logo-save")= currentSetting.textBelowCustomLoginLogo
li.tableVisibilityMode-form
.title {{_ 'login-background-color'}}
.form-group
input.wekan-form-control#login-background-color(type="text", placeholder="#1b365d" value="{{currentSetting.loginBackgroundColor}}")
li.tableVisibilityMode-form
.title {{_ 'custom-top-left-corner-logo-image-url'}}
.form-group
@@ -811,6 +811,7 @@ Template.tableVisibilityModeSettings.events({
['#custom-login-logo-image-url', 'customLoginLogoImageUrl'],
['#custom-login-logo-link-url', 'customLoginLogoLinkUrl'],
['#text-below-custom-login-logo', 'textBelowCustomLoginLogo'],
['#login-background-color', 'loginBackgroundColor'],
['#custom-top-left-corner-logo-image-url', 'customTopLeftCornerLogoImageUrl'],
['#custom-top-left-corner-logo-link-url', 'customTopLeftCornerLogoLinkUrl'],
['#custom-top-left-corner-logo-height', 'customTopLeftCornerLogoHeight'],
+21 -3
View File
@@ -1039,12 +1039,30 @@ Template.addListInline.events({
// #2809
'click .js-link-list': Popup.open('linkList'),
'click .js-close-add-list-inline'() {
Session.set('wekan-add-list-after', null);
Session.set('wekan-add-list-swimlane', null);
Session.set('wekan-add-list-empty', null);
closeAddListComposer();
},
});
// Close the inline add-list composer (the one opened after a list, or in an empty
// swimlane). The X in the composer and Escape do the same thing.
function closeAddListComposer() {
Session.set('wekan-add-list-after', null);
Session.set('wekan-add-list-swimlane', null);
Session.set('wekan-add-list-empty', null);
}
// #6865: Escape closes the add-list composer. Only the X did: the composer stays
// open after a list is created, so one can add several, and then Escape did
// nothing. It is closed at the level of an inlined form (below popups and modals,
// above the card's details and the sidebar), and not by a click elsewhere, as an
// inlined form is not.
EscapeActions.register(
'inlinedForm',
closeAddListComposer,
() => !!(Session.get('wekan-add-list-after') || Session.get('wekan-add-list-empty')),
{ enabledOnClick: false },
);
Template.listsGroup.helpers({
// #6745: board-role checks only (see listHeader.js) - not the whole user.
currentUser() {
+7
View File
@@ -439,6 +439,13 @@ template(name="changeSettingsPopup")
| {{_ 'keep-title-tags'}}
if isKeepTitleTags
i.fa.fa-check
//- #6755: the blue focus outline (an accessibility feature, so on by default).
li
a.js-toggle-hide-focus-outline
i.fa.fa-square-o
| {{_ 'show-focus-outline'}}
if isShowFocusOutline
i.fa.fa-check
//- #6827: the Add card form stays open after a card is added.
li
a.js-toggle-keep-add-card-form-open(title="{{_ 'keep-add-card-form-open-description'}}")
+8
View File
@@ -503,6 +503,11 @@ Template.changeSettingsPopup.helpers({
const currentUser = ReactiveCache.getCurrentUser();
return currentUser ? currentUser.hasKeepTitleTags() : false;
},
// #6755: the outline is shown unless this member hid it.
isShowFocusOutline() {
const currentUser = ReactiveCache.getCurrentUser();
return currentUser ? !currentUser.hasHideFocusOutline() : true;
},
isKeepAddCardFormOpen() {
const currentUser = ReactiveCache.getCurrentUser();
return currentUser ? currentUser.hasKeepAddCardFormOpen() : false;
@@ -569,6 +574,9 @@ Template.changeSettingsPopup.events({
'click .js-toggle-unread-changes'() {
if (ReactiveCache.getCurrentUser()) Meteor.call('toggleUnreadChanges');
},
'click .js-toggle-hide-focus-outline'() {
if (ReactiveCache.getCurrentUser()) Meteor.call('toggleHideFocusOutline');
},
'click .js-toggle-keep-add-card-form-open'() {
if (ReactiveCache.getCurrentUser()) Meteor.call('toggleKeepAddCardFormOpen');
},
+23
View File
@@ -0,0 +1,23 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
// #6870: whether the administrator blocked attachment uploads for the whole site
// (models/lib/attachmentUploadPolicy.js). Asked of the server once a minute at most; until it
// answers - or if it cannot - uploads are shown as before, because the server refuses them either
// way and a wrong guess here only costs a button.
const blocked = new ReactiveVar(false);
let askedAt = 0;
export function refreshUploadPolicy() {
if (Date.now() - askedAt < 60 * 1000) return;
askedAt = Date.now();
Meteor.callAsync('attachmentUploadPolicy')
.then(policy => blocked.set(!!(policy && policy.uploadsBlocked === true)))
.catch(() => { askedAt = 0; });
}
// reactive
export function attachmentUploadsBlocked() {
refreshUploadPolicy();
return blocked.get();
}
+9 -4
View File
@@ -1,12 +1,14 @@
import { Template } from 'meteor/templating';
import { ReactiveCache } from '/imports/reactiveCache';
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
// #4475: Admin Panel -> Settings -> Visibility -> "Only admins can create
// boards" (tableVisibilityMode-boardCreationAdminOnly). Shared by every
// client-side "Add board" entry point so the button hides consistently;
// the real enforcement is server-side, in createBoardWithInitialSwimlanes
// (server/models/boards.js) - this only avoids showing a control that
// would be rejected.
// client-side entry point that makes a new board - Add Board, and with it
// Import and Template, Duplicate in the All Boards sidebar, and "Create
// board from card" - so they hide consistently. The real enforcement is on
// the server, on every path (CreateBleed, server/lib/boardCreationGuard.js);
// this only avoids offering a control the server would refuse.
export function boardCreationAllowed() {
const restricted = TableVisibilityModeSettings.findOne(
'tableVisibilityMode-boardCreationAdminOnly',
@@ -16,3 +18,6 @@ export function boardCreationAllowed() {
}
return !!ReactiveCache.getCurrentUser()?.isAdmin;
}
// For templates: {{#if boardCreationAllowed}}.
Template.registerHelper('boardCreationAllowed', () => boardCreationAllowed());
+10 -29
View File
@@ -74,35 +74,16 @@ hotkeys.filter = (event) => {
};
// Handle non-Latin keyboards
window.addEventListener('keydown', (e) => {
// Only handle event if coming from body
if (e.target !== document.body) return;
// Only a character typed on a non-Latin layout: one non-ASCII character.
// Comparing it with the key code's lowercase letter also matched every
// SHIFTED Latin key ('D', '!', and 'Shift' itself), and re-dispatched it, so
// each Shift shortcut ran twice: Shift+D opened the due date editor and the
// copy clicked the same control again, closing it (#6750).
if (typeof e.key !== 'string' || e.key.length !== 1 || e.key.charCodeAt(0) < 128) return;
// Trigger the corresponding action by dispatching a new event with the ASCII key
const key = String.fromCharCode(e.which).toLowerCase();
if (!/^[a-z0-9]$/.test(key)) return;
// Create a synthetic event for hotkeys to handle, with the same modifiers,
// so Shift shortcuts keep working on non-Latin layouts.
const syntheticEvent = new KeyboardEvent('keydown', {
key: key,
keyCode: e.which,
which: e.which,
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
metaKey: e.metaKey,
bubbles: true,
cancelable: true,
});
document.dispatchEvent(syntheticEvent);
});
//
// Nothing to do here, on purpose (#6840). This used to re-dispatch a key typed on
// another script (a Farsi or Cyrillic letter) as its Latin key, for the old
// Mousetrap, which read the typed character. hotkeys-js does not: it resolves a
// key from `event.code` ('KeyD') and `keyCode`, whatever character the layout
// types, so it already runs the shortcut for the physical d key on any layout.
// The copy then ran every shortcut a SECOND time - the due date editor opened
// and the repeat clicked the same control, closing it again, so `d` "opens the
// popup and closes it at a glance" on a Farsi keyboard (and Shift+D did the same
// on a Latin one, #6750). tests/keyboardNonLatinOnce.test.cjs.
function getHoveredCardId() {
const card = $('.js-minicard:hover').get(0);
+22 -10
View File
@@ -20,17 +20,29 @@ only what changes: documents, files and log lines. It writes them seconds after
they happen, to a directory that can be another disk, an NFS share or an
[rclone mount](Rclone/Rclone.md).
## One server only, for now
## Several servers: one of them streams
Continuous backup is for installations with **one WeKan server**. When it is
switched on in the Admin Panel, every WeKan server that shares the database
applies the setting, so with several servers (for example the replicas of a
Kubernetes Deployment) each of them tails the oplog and writes its own archives
into the same target directory: the same backup stream, many times, into one
place. It has no leader: one server should run the stream and another take it
over when that one stops (a lease), and that is not built yet
([#6861](https://github.com/wekan/wekan/issues/6861)). With several servers, use
the database's own backup instead.
When WeKan runs as several servers on one database (for example the replicas of a Kubernetes
Deployment), **one of them runs the stream** and the others stand by
([#6861](https://github.com/wekan/wekan/issues/6861)). They coordinate through a *lease*: one
document, `continuousBackupLease`, holding the leader's id and an expiry.
- Every server with the feature switched on tries to take the lease. Taking it is one atomic
database operation, so two servers can never both lead.
- The leader renews it every 15 seconds. It lapses 45 seconds after the last renewal, so when the
leader stops (a pod restart, a rollout, a crash) another server takes over within about a minute
and continues from the position the stream saved in the target.
- A server that finds it has lost the lease stops its stream, so two never write one target.
- Saving the settings, switching the feature off and on, and a change saved on another server all
converge through the same loop: the leader follows the change on its next tick.
- A restore, and *fetch from the cloud*, must be the only writer: they take the lease from whoever
holds it and wait one renewal, so the former leader has stopped before anything is written. The
stream resumes through the loop afterwards.
An installation with one server is always its own leader, so nothing changes for it. The target
directory is still one place: with several servers it must be on storage they all reach (a shared
volume, NFS, an [rclone mount](Rclone/Rclone.md)), or the server that takes over starts a new
generation instead of continuing.
## Licences: only copyfree code by default
+221 -19
View File
@@ -1,3 +1,13 @@
The full-scale tables below were measured on 2026-10-23 with FerretDB v1.99.0.
Every change in this section came after it (FerretDB v1.100.0 to v1.105.0,
2026-10-24 to 2026-10-31, WeKan up to 2026-11-02), so **those tables do not show
them**; [Results.md](Results.md) is a new, small-scale run that does. The
figures quoted in this section are the ones in FerretDB's own
[CHANGELOG](https://github.com/wekan/FerretDB/blob/main-v1/CHANGELOG.md), each
measured on a smaller database (61,507 cards, 92,000 boards or the 615,070 cards
of this test, as stated), so they show the size of a change, not a place in the
table.
# WeKan database speed test
How fast does WeKan's database answer the queries WeKan actually makes - on
@@ -20,10 +30,13 @@ exist, which images they use or how they are started:
| Database | How it runs |
| --- | --- |
| FerretDB v1 + SQLite | FerretDB built from `.tools/FerretDB` (the newest source), data under `.tools/tmp` |
| FerretDB v1 + SQLite | FerretDB built from `.tools/w/FerretDB` (the newest source), data under `.tools/tmp` |
| FerretDB v1 + PostgreSQL | `postgres:18`, the image of [docker-compose-ferretdb-v1-postgresql.yml](../../../../../docker-compose-ferretdb-v1-postgresql.yml) |
| FerretDB v1 + MySQL | `mysql:9`, from [docker-compose-ferretdb-v1-mysql.yml](../../../../../docker-compose-ferretdb-v1-mysql.yml) |
| FerretDB v1 + MariaDB | `mariadb:12`, from [docker-compose-ferretdb-v1-mariadb.yml](../../../../../docker-compose-ferretdb-v1-mariadb.yml) |
| FerretDB v1 + pgrust | `malisper/pgrust:v0.4-beta`: [PostgreSQL rewritten in Rust](https://github.com/malisper/pgrust), wire compatible, so FerretDB's PostgreSQL handler talks to it unchanged. In the conformance run only with `WEKAN_CONFORMANCE_PGRUST=1` |
| FerretDB v1 + TiDB | `pingcap/tidb:latest` (Apache-2.0, MySQL protocol) as one process on its own storage, no PD or TiKV. In the conformance run only with `WEKAN_CONFORMANCE_TIDB=1` |
| FerretDB v1 + Dolt | `dolthub/dolt-sql-server:latest`: [git for data](https://github.com/dolthub/dolt), MySQL protocol, run as it comes (no commit per write). In the conformance run only with `WEKAN_CONFORMANCE_DOLT=1`. What its version history gives WeKan's undo is in [VersionHistory](../VersionHistory/) |
| MongoDB 6, 7, 8, 9 | `mongo:6.0`, `mongo:7` ([docker-compose-mongodb-v7.yml](../../../../../docker-compose-mongodb-v7.yml)), `mongo:8.0`, `mongo:9.0` - queried directly, no FerretDB |
| FerretDB v1 + SAP HANA | only with `WEKAN_CONFORMANCE_HANA=1` (amd64, ~16 GB RAM, SAP's licence) |
| FerretDB 2 + PostgreSQL with DocumentDB | `ghcr.io/ferretdb/ferretdb:2` and `ghcr.io/ferretdb/postgres-documentdb:17`, the images of [docker-compose-ferretdb-v2-postgresql.yml](../../../../../docker-compose-ferretdb-v2-postgresql.yml), on a network of their own; in the conformance run only with `WEKAN_CONFORMANCE_FERRETDB2=1` |
@@ -59,7 +72,11 @@ the busy users.
It is **deterministic**: a fixed seed and a seeded generator, never
`Math.random()`, so every database is loaded with byte-for-byte identical
documents - which is what makes it possible to ask whether they answer the same.
`WEKAN_SPEEDTEST_SCALE` changes the size (1 = production, 0.05 for a quick try).
`WEKAN_SPEEDTEST_SCALE` changes the size (1 = production, 2 = twice that, the
default). **`WEKAN_SPEEDTEST_SCALE=0.05` is 1/20 of a production WeKan - 15,377
cards - and the whole run, every database, takes under half an hour instead of
hours** (with the images already pulled) (the SQLite indexes alone take 19 minutes at scale 2 and 7 seconds at
0.05). Results.md says which scale it was measured at.
After loading, the test creates **the indexes WeKan creates**. They are not
listed here: [indexes.cjs](indexes.cjs) reads every `ensureIndex(...)` call in
@@ -135,6 +152,12 @@ Things to know when reading the numbers:
That costs FerretDB some load and write time MongoDB does not pay here.
- Each database runs with its image's default settings - what an administrator
gets - not tuned.
- Results.md was measured at scale 0.05 (see "The data"), with FerretDB v1.105.0
plus the changes of "Improvements since the last run". A row's FerretDB commit
is in its name; the rows differ because a database is re-run alone after a
change to FerretDB, which does not touch the others. The full-scale tables
further down are from 2026-10-23 (v1.98 and v1.99) and are kept as the record
of what each change was worth.
- A row's FerretDB commit is in its name: the databases were measured as their
FerretDB fixes went in (the PostgreSQL row after its index fix, MySQL and
MariaDB after their `_id` fix).
@@ -208,28 +231,207 @@ and its board scope is a plain `boardId` list when no board is assigned-only. It
docker-compose for FerretDB 2 could not start with the current DocumentDB image
(`POSTGRES_DB` must be `postgres`).
## Improvements since the last run
[Results.md](Results.md) was measured on 2026-10-23 with FerretDB v1.99.0. Every
change below came after it (FerretDB v1.100.0 to v1.105.0, 2026-10-24 to
2026-10-31, and WeKan up to 2026-11-02), so **the tables in Results.md do not
show them yet** - run the speed test again for full-scale numbers. The figures
quoted here are the ones in FerretDB's own
[CHANGELOG](https://github.com/wekan/FerretDB/blob/main-v1/CHANGELOG.md), each
measured on a smaller database (61,507 cards, 92,000 boards or the 615,070 cards
of this test, as stated), so they show the size of a change, not a place in the
table.
**FerretDB on SQLite**
- **SQLite sorts in the database** (v1.100.0). A sorted query used to read,
decode and keep every matching document in Go to return a few - People's ten
newest users, the first page of a list. SQLite now orders the row ids by a key
that reproduces MongoDB's order for simple values, and documents are fetched a
batch at a time in that order: the ten newest of 30,000 users allocate 2.6 MB
instead of 264 MB. A document whose sort field holds an array, a document, NaN
or an infinity comes first, and then everything is sorted in Go as before, so
the answer is the same. The key is read once per row, in one JSON path.
- **Documents are stored as SQLite JSONB** (v1.101.0). Every filter, sort key,
index and multikey trigger used to parse the JSON text of a document again.
New collections are binary, and an existing database is converted once when
it is opened, table by table in a transaction (204,212 documents took 81 s; a
table that fails stays text and keeps working). On 61,507 cards, due cards went
from 1,003 to 519 ms, All Boards from 95 to 42 ms and creating the 44 indexes
from 107 to 70 s. After the conversion an older FerretDB cannot read the
database.
- **`distinct` works when documents lack the field** (v1.105.0), which a database
migrated from MongoDB 3 needs on its first start.
- **A membership test reads the array from the column, not from the text of `->`**
(found by profiling Due Cards, fixed after v1.105.0). `{members: {$elemMatch:
{userId: X}}}` was `json_each(col->"members")`: `->` renders the whole array as
JSON text and `json_each` parses that text again, for every row. It is now
`json_each(col, '$."members"')`, which SQLite walks in place. The visible-boards
query went from 26.2 to 15.7 ms on 2,308 boards, and in the speed test All Boards
count from 6.9 to 4.9 ms, All Boards page from 5.9 to 3.9 ms and Due Cards from
54.5 to 43.9 ms (the SQLite total 82.7 → 68.3 ms); the 169 answers of 12 queries
are identical to before.
**FerretDB on PostgreSQL**
- **Membership tests use a GIN index** (v1.100.0): an index whose leading key is
`ARRAY.FIELD` (`members.userId`) also gets a `jsonb_path_ops` GIN index of the
array, and `{members: {$elemMatch: {userId: X}}}` uses it.
- **`$or` and `$elemMatch` are conditions in the database** (v1.101.0), so Due
Cards' query went from 2,031 to 815 ms on 61,507 cards.
- **A top-level `$and` is pushed down** (v1.100.0). WeKan's Due Cards wraps its
conditions in one, and every document was read.
- **A table is analyzed once its indexes are created** (found by the small run,
fixed after v1.105.0). PostgreSQL has statistics for an expression index
(`_jsonb->'field'`) only after `ANALYZE`, which autovacuum runs a minute or
more after a bulk load. Before that the planner guessed **one row** for a Due
Cards query that returns 2,000, and chose a plan with no parallelism: 407 ms
on 15,377 cards, 84 ms after. The whole PostgreSQL row went from 454 ms to
145 ms and from last place to eighth. The same `ANALYZE TABLE` was tried on
MySQL and MariaDB and made no difference (100 and 279 ms before, 106 and 283
after), so it was not kept.
**FerretDB on MySQL and MariaDB**
- **Reads use their indexes** (v1.100.0): an equality or `$in` narrows through
the index's generated column. A card by `_id` among 615,070 went from 3.4 s
(MySQL) and 18.7 s (MariaDB) to 1.8 ms. Generated columns are `VIRTUAL`, so
adding the 44 indexes of a 600,000-document database takes 157 s, not 20
minutes; an index of four or more fields indexes prefixes, so it fits the
3,072-byte key limit that three of WeKan's compound indexes used to exceed;
and a path with a dash or a digit (`services.accounts-lockout.unlockTime`, `profile.emailBuffer.0`) no longer
fails with "Invalid JSON path expression".
- **Multikey side tables** (v1.100.0), as on SQLite, so a membership test reads
only the matching documents. Without the right to create triggers (MySQL with
binary logging and no SUPER) nothing is created and queries read as before.
- **An `$or` of `_id` sets is one `UNION ALL`** (v1.100.0): All Boards for a user
with a starred board went from 900 ms (MySQL) and 5.7 s (MariaDB) among 92,000
boards to 6-7 ms.
- **A long `$in` is one `IN`** (v1.101.0), not 2,596 `JSON_CONTAINS` in an OR
(52 million comparisons over 20,000 cards).
- **Updates and deletes by `_id` use the `_id` index** (v1.99.0, listed above).
**WeKan** - less work for the database, which matters more the bigger it is:
- **Collection scans are gone from the hourly and startup paths.** The recurring
cards job has a partial index on `recurrenceInterval` (it read 1,108,017 cards
in 1,155 ms every hour); search sessions have the indexes their upsert and
lookup use, and the stale sessions of every user are removed (one instance had
4,751 taking 10 GB); the board key backfill is one `$in` on the key index; the
list sync scan and the due-date lookup have partial and sparse indexes.
- **One server does it, not all of them.** The startup schema re-check is claimed
in `_wekan_migration`, the daily due-date reminder scan and the database
health probe are claimed per period, so 24 replicas no longer scan, send or
report 24 times.
- **Admin Panel / Problems / Database** reports what makes an instance slow: a
query that read a whole collection of at least 10,000 documents (which
collection, the command, the operators per field and the `queryHash`), and a
high read rate. The speed test's Due Cards and All Boards numbers say what a
query costs on one database; this says which query an instance is actually
asking.
## What could still be improved
- **Sorting and `limit` in the database.** FerretDB v1 sorts in Go: People's ten
newest users, All Boards' first page and a card window read and sort every
matching document. Pushing an ORDER BY down needs MongoDB's ordering across
types, missing fields and arrays to be exact in SQL.
- **Due Cards on SQLite, 44 ms of the 68 ms total.** A CPU profile of FerretDB
while it ran Due Cards (taken before the change above): half the CPU was inside
SQLite, and about a fifth of all of it converting JSON between binary and text
(`_jsonTranslateBlobToText` 10%, `_jsonTranslateTextToBlob` 10%, besides
`_jsonLookupStep`); another quarter was Go runtime (thread signalling,
`madvise`, the network poller). Of the cards query's 24 ms, 18 ms is the index
lookups (the 700-board `IN` list) and 6 ms the five per-user conditions
(`members`, `assignees`, `requesters`, `assigners`, `userId`), each of which
evaluates `->` three times per row. Reading each once, as
`json_type(col,'$."f"') = 'array' OR col->>'$."f"' = ?`, took that step from
24.4 to 20.5 ms with the same rows - **not done**, because it may only be applied
to a field no index covers (an index is matched by the expression `col->"f"`),
and FerretDB's WHERE builder does not know which fields are indexed.
- **Tried on SQLite and not worth it, or not conclusive**: `ANALYZE` made no
difference (FerretDB chooses its indexes itself); a Unix socket instead of TCP
saves about 0.05 ms (20%) on a point read (0.24 → 0.19 ms), which is free when
WeKan and FerretDB share a machine (`--listen-unix`); `GOGC=400` gave 10-15% on
some queries in one test (board cards 5.3 → 4.4 ms) and nothing in another that
also set a memory limit, so it is a lead, not a result.
- **The busiest users.** What remains of All Boards for a member of thousands of
boards, and of Due Cards, is decoding those boards and sorting them in Go.
- **SQLite stores JSON as text.** Every `->` parses the document again - which is
also why creating an index takes 19 minutes for SQLite and 115 s for PostgreSQL,
whose `jsonb` is binary. Storing SQLite's JSONB would remove that, but changes
the storage format and needs a migration.
- **Multikey indexes for PostgreSQL, MySQL and MariaDB.** Only SQLite has the side
tables; All Boards takes 8-9 s on the others.
- **MySQL and MariaDB reads do not use their indexes**: equality is
`JSON_CONTAINS(JSON_EXTRACT(doc, ?))`, so a card by `_id` takes 3.4 s and 18.7 s
(and an edit as long - it reads the card first). Their indexes are generated
`VARCHAR(255)` columns added with a table rebuild each (20 and 40 minutes for
44 indexes; VIRTUAL columns would avoid the rebuild), and three of WeKan's
compound indexes exceed the 3,072-byte key limit and are not created.
boards, and of Due Cards, is decoding those boards and sorting them in Go on
the databases that do not sort in SQL.
- **Sorting in the database on PostgreSQL, MySQL and MariaDB.** Only SQLite does
it. PostgreSQL was tried (2026-11-03): an `ORDER BY` of type class, number and
text (`COLLATE "C"`, MongoDB's byte order) with the documents that cannot be
ordered first, as on SQLite, and the tests matched the Go sort over mixed
types. It was **not faster**: the ten newest of 30,116 users took 52 ms, against
38-49 ms for fetching, decoding and sorting them in Go, because the Go path is
already fast after the decoding changes and the sort expressions cost about as
much. It would only save memory and transfer, so it was not kept. A sort that
wins needs an expression index on the sort field, which has to know the field
in advance; MySQL and MariaDB have the same cost structure and no measurement
that says otherwise.
- **MariaDB stores JSON as text** that every JSON function parses again, so it
keeps only what an index answers (`$or` and `$elemMatch` are not pushed down).
- **Existing MySQL/MariaDB tables** keep their case-insensitive index columns
until rebuilt.
- **Measuring it again at full scale.** Results.md is scale 0.05. A full-scale
run (`WEKAN_SPEEDTEST_SCALE=2`, hours) would show the busiest users, where the
differences are largest.
## Other databases tried
Asked for: any other fast database that FerretDB can use and that is open source.
FerretDB v1 has handlers for PostgreSQL, MySQL, SQLite and SAP HANA, so a
candidate has to speak one of those protocols **and** run the SQL the handler
writes: `jsonb` expression indexes, `@>` and GIN on PostgreSQL; `JSON`,
`JSON_EXTRACT`, generated columns and triggers on MySQL.
| Database | Result |
| --- | --- |
| [pgrust](https://github.com/malisper/pgrust) v0.4-beta (PostgreSQL in Rust, AGPL-3.0) | **Works**, same answers as MongoDB, and is in Results.md. It is not faster than PostgreSQL for WeKan's queries: 174 ms against 145 ms in total, and creating the 47 indexes takes 38 s against 1 s. |
| [TiDB](https://github.com/pingcap/tidb) 7.5 (Apache-2.0, MySQL protocol) | **Works**, same answers, and is in Results.md (240 ms, between pgrust and MariaDB). It needed one change in FerretDB: its metadata table's generated columns are `VIRTUAL`, because TiDB refuses `ALTER TABLE ... ADD COLUMN ... STORED`. This is one process with its own storage; a real cluster (PD and TiKV) is a different test. |
| [Dolt](https://github.com/dolthub/dolt) 2.6.2 (git for data, Apache-2.0, MySQL protocol) | **Works with the existing MySQL handler, no new handler**, same answers as MongoDB, and is in Results.md - and is **by far the slowest: 20,470 ms in total, 1,230x MongoDB**. Every query over cards takes about 2.8 s on 15,377 cards (a whole-table scan, about 187 µs a document: the generated-column indexes the handler relies on do not narrow a read there), All Boards 850 ms, and creating the indexes 106 s. Its point is version history, not speed; see [VersionHistory](../VersionHistory/). |
| [Noria](https://github.com/mit-pdos/noria) (MIT research prototype, Apache-2.0) | **Cannot run FerretDB.** It is a streaming dataflow system that precomputes the results of a fixed set of prepared queries. Its SQL has no `JSON` type (the parser's type list is `INT`, `VARCHAR`, `TEXT`, `BLOB`, `DATE`, ...), no triggers and no generated columns, and the MySQL handler needs all three: it uses `JSON` 79 times, `JSON_EXTRACT` 74 times and 7 generated columns. It also needs unpinned nightly Rust (2021) and ZooKeeper, and has not changed since October 2021. Not built or run. |
| [YugabyteDB](https://github.com/yugabyte/yugabyte-db) 2026.1.2 (core Apache-2.0, PostgreSQL 15 compatible) | **Cannot run FerretDB.** Started, and FerretDB's first `CREATE INDEX` fails: `INDEX on column of type 'JSONB' not yet supported`. Every FerretDB v1 index on PostgreSQL is an expression of type `jsonb`, and so is every query that uses one; YugabyteDB can index a `jsonb` column only with GIN. Supporting it means writing the indexes and conditions as `->>` text, which is a different handler. |
| [CockroachDB](https://github.com/cockroachdb/cockroach) | **Not tried: not open source.** Its LICENSE is the proprietary CockroachDB Software License, not an open-source one. |
Tried with the MySQL handler, to see how far it gets (both speak the MySQL protocol):
| Database | Result |
| --- | --- |
| [ClickHouse](https://github.com/ClickHouse/ClickHouse) 26.10 (Apache-2.0) | **Needs a handler of its own.** The first statement FerretDB sends through its MySQL interface fails: `sql: expected 0 arguments, got 2`. Behind that it has no `JSON_EXTRACT`, `JSON_CONTAINS`, triggers or generated columns as the handler uses them (it has `JSONExtract*`), and a row update is an asynchronous mutation, which is not what WeKan's edits and moves are. |
| [StarRocks](https://github.com/StarRocks/starrocks) 4.1 (Apache-2.0) | **Needs a handler of its own.** The first `CREATE TABLE` fails: `Data type of first column cannot be JSON`; its tables need a key column and a distribution. An analytical engine again, with no triggers. |
A handler for either is a new backend of the size of the SAP HANA one (about
2,000 lines of Go: query, insert, update, delete, indexes, statistics) plus its own
SQL for every filter FerretDB can push down; without the pushdown a query reads
the whole collection, which the speed test would measure as slow. That has not
been written. Not tried at all: DuckDB (no server; the Go driver needs cgo, which
FerretDB's builds do not use) and SingleStore (not open source, and its image
needs a licence key). A MongoDB-protocol database needs no FerretDB and joins the
MongoDB rows; FerretDB 2 with DocumentDB is already there.
## Opening a board in WeKan
The same databases, one level up: [app/](app/) opens a WeKan board of 200, 2,000 and
5,000 cards on FerretDB (SQLite) and on MongoDB 8 and profiles both the Meteor server
and FerretDB. The Meteor server's CPU is the same on both (0.1-0.5 s); on a 5,000-card
board FerretDB (SQLite) takes **400-600 ms longer** (to the first card +42-48%), a
small board only 70-100 ms, and it uses about 4 s of CPU to open the 5,000-card board,
a third of it filtering in Go what it read; the browser receives the same ~210 KB at
every size.
## Go databases linked into FerretDB
Could a database implemented in Go be linked into the FerretDB binary beside its
SQLite, and be faster? [embedded/](embedded/) asks that of the
[awesome-go list](https://github.com/avelino/awesome-go#databases-implemented-in-go):
Pebble, Badger, bbolt, BuntDB, Nutsdb and Clover against SQLite set up as FerretDB
sets it, on the speed test's cards, in minutes. **None is faster where it counts**:
a board's cards take the same time on every engine that is not slower, because
decoding the JSON dominates; the key-value stores win single reads and writes by
microseconds that the 0.2 ms floor of a FerretDB call hides, and would make the
handler do in Go what SQL does in SQLite. bbolt with its default fsync is 100-160x
slower on writes, Pebble 12x slower on fetching a board, Clover 90-190x slower.
## FerretDB 2
+76 -40
View File
@@ -1,12 +1,34 @@
# WeKan database speed test results
Run 2026-10-23 on Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, 12 threads, 30 GiB RAM, Linux 7.2.6-070206-generic, by
Run 2026-11-03 on Apple M2 Max, 12 threads, 32 GiB RAM, Darwin 27.2.0, by
[speedtest.sh](speedtest.sh). How it works, and what was improved because of it:
[the README](.).
The data is twice a production WeKan: 30,116 users, 92,306 boards, 221,038 lists, 222,546 swimlanes, 615,070 cards (production: 307,535 cards). Every database got identical
The data is 1/20 of a production WeKan: 753 users, 2,308 boards, 5,526 lists, 5,564 swimlanes, 15,377 cards (production: 307,535 cards). Every database got identical
documents and WeKan's own indexes, and was asked identical questions.
## At a glance
One number per database: the time of one call of each of the common WeKan queries below,
added together. **Times slower** is that sum divided by the fastest database's. Ordered
from fastest to slowest. The second pair of columns is the same for the slowest parameter -
the biggest board or the busiest user, who waits longest.
| # | Database | Total (ms) | Times slower | Slowest user (ms) | Times slower |
| ---: | --- | ---: | ---: | ---: | ---: |
| 1 | MongoDB 9.0.2 | 16.6 | 1.0x | 45.1 | 1.0x |
| 2 | MongoDB 8.0.32 | 17.4 | 1.0x | 48.8 | 1.1x |
| 3 | MongoDB 6.0.28 | 18.0 | 1.1x | 50.2 | 1.1x |
| 4 | MongoDB 7.0.43 | 18.1 | 1.1x | 50.1 | 1.1x |
| 5 | FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | 57.9 | 3.5x | 154 | 3.4x |
| 6 | FerretDB v1.105.0 (9030e55c) + SQLite (modernc.org/sqlite v1.61.0) | 68.3 | 4.1x | 219 | 4.9x |
| 7 | FerretDB v1.105.0 (0db7b801) + MySQL 9.7.2 | 103 | 6.2x | 415 | 9.2x |
| 8 | FerretDB v1.105.0 (0c784f2c) + PostgreSQL 18.6 | 145 | 8.7x | 483 | 11x |
| 9 | FerretDB v1.105.0 (0c784f2c) + pgrust 0.4-beta (PostgreSQL 18.6 compatible) | 174 | 10x | 594 | 13x |
| 10 | FerretDB v1.105.0 (0db7b801) + TiDB v7.5.1 | 240 | 14x | 877 | 19x |
| 11 | FerretDB v1.105.0 (0db7b801) + MariaDB 12.3.3 | 290 | 17x | 1,063 | 24x |
| 12 | FerretDB v1.105.0 (0db7b801) + Dolt 2.6.2 | 20,470 | 1,230x | 20,904 | 464x |
## Speed
Milliseconds for one call - smaller is faster. Each query is asked with 15 parameters (the
@@ -16,66 +38,80 @@ fastest database at the top.
| Database | Total | Login | User by id | All Boards count | All Boards page | Board by id | Board lists | Board swimlanes | Board cards | Card window | Card count | Cards per list | Card by id | Card search | Due Cards | People page | Edit card | Move card | Add card |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| MongoDB 9.0.4 | **495** | 0.75 | 0.66 | 5.76 | 16.4 | 0.68 | 1.06 | 0.98 | 47.7 | 0.77 | 1.61 | 2.62 | 0.66 | 5.34 | 394 | 13.6 | 0.77 | 1.16 | 1.00 |
| MongoDB 6.0.28 | **562** | 0.78 | 0.73 | 5.49 | 21.0 | 0.83 | 1.01 | 0.93 | 48.2 | 0.89 | 2.01 | 4.38 | 0.74 | 4.76 | 447 | 20.9 | 0.83 | 0.90 | 0.77 |
| MongoDB 7.0.45 | **565** | 0.86 | 0.74 | 6.29 | 23.7 | 0.79 | 1.07 | 1.00 | 48.9 | 0.87 | 2.00 | 4.35 | 0.72 | 5.52 | 445 | 20.7 | 0.79 | 0.77 | 0.76 |
| MongoDB 8.0.34 | **577** | 0.81 | 0.69 | 6.83 | 16.8 | 0.77 | 1.19 | 1.01 | 48.5 | 0.88 | 1.90 | 2.87 | 0.69 | 6.28 | 470 | 15.6 | 0.78 | 0.89 | 0.78 |
| FerretDB v1.99.0 (b6c494e9) + SQLite (modernc.org/sqlite v1.61.0) | **18,848** | 0.99 | 0.93 | 1,405 | 1,655 | 1.08 | 4.15 | 2.94 | 223 | 67.2 | 86.0 | 119 | 0.89 | 81.7 | 12,604 | 2,582 | 4.69 | 6.36 | 2.32 |
| FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | **88,884** | 1.35 | 1.03 | 183 | 241 | 1.15 | 1.72 | 1.68 | 113 | 53.5 | 43.5 | 46.0 | 0.93 | 46.1 | 88,013 | 118 | 7.89 | 6.09 | 6.20 |
| FerretDB v1.99.0 (b6c494e9) + MariaDB 12.3.3 | **97,536** | 1.76 | 2.05 | 11,389 | 11,396 | 1.82 | 3.41 | 2.43 | 189 | 2.43 | 171 | 173 | 1.79 | 171 | 73,217 | 791 | 9.24 | 8.49 | 6.68 |
| FerretDB v1.99.0 (b6c494e9) + PostgreSQL 18.6 | **98,435** | 1.24 | 1.21 | 159 | 149 | 1.36 | 2.14 | 1.62 | 217 | 1.46 | 121 | 132 | 1.14 | 137 | 96,035 | 1,444 | 9.51 | 11.9 | 9.48 |
| FerretDB v1.99.0 (b6c494e9) + MySQL 9.7.4 | **106,082** | 1.86 | 3.09 | 1,073 | 1,082 | 1.86 | 2.90 | 2.37 | 249 | 4.88 | 133 | 140 | 1.74 | 153 | 101,505 | 1,687 | 14.4 | 15.7 | 11.6 |
| MongoDB 9.0.2 | **16.6** | 0.48 | 0.41 | 0.89 | 0.87 | 0.43 | 0.42 | 0.43 | 2.08 | 0.42 | 0.45 | 0.53 | 0.35 | 0.60 | 6.23 | 0.68 | 0.39 | 0.52 | 0.48 |
| MongoDB 8.0.32 | **17.4** | 0.46 | 0.41 | 0.86 | 0.85 | 0.39 | 0.48 | 0.45 | 2.14 | 0.42 | 0.49 | 0.52 | 0.35 | 0.63 | 7.09 | 0.78 | 0.38 | 0.40 | 0.34 |
| MongoDB 6.0.28 | **18.0** | 0.44 | 0.46 | 0.71 | 0.96 | 0.43 | 0.44 | 0.41 | 2.13 | 0.39 | 0.43 | 0.50 | 0.36 | 0.60 | 7.42 | 0.82 | 0.49 | 0.49 | 0.53 |
| MongoDB 7.0.43 | **18.1** | 0.45 | 0.41 | 0.71 | 0.91 | 0.41 | 0.46 | 0.41 | 2.05 | 0.40 | 0.42 | 0.51 | 0.36 | 0.55 | 7.81 | 0.76 | 0.46 | 0.52 | 0.48 |
| FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | **57.9** | 0.67 | 0.55 | 2.10 | 2.82 | 0.69 | 0.71 | 0.56 | 3.21 | 1.12 | 1.06 | 1.29 | 0.52 | 1.42 | 33.8 | 1.91 | 1.87 | 1.77 | 1.85 |
| FerretDB v1.105.0 (9030e55c) + SQLite (modernc.org/sqlite v1.61.0) | **68.3** | 0.29 | 0.23 | 4.35 | 3.40 | 0.28 | 0.42 | 0.37 | 4.91 | 0.72 | 1.69 | 2.36 | 0.22 | 1.01 | 40.7 | 3.90 | 1.36 | 1.39 | 0.67 |
| FerretDB v1.105.0 (0db7b801) + MySQL 9.7.2 | **103** | 0.92 | 0.87 | 6.13 | 6.40 | 0.91 | 0.97 | 0.88 | 7.79 | 1.04 | 5.05 | 5.13 | 0.85 | 5.75 | 35.9 | 16.9 | 2.65 | 2.87 | 2.08 |
| FerretDB v1.105.0 (0c784f2c) + PostgreSQL 18.6 | **145** | 0.76 | 0.67 | 5.84 | 6.14 | 0.59 | 0.57 | 0.55 | 7.70 | 0.70 | 4.54 | 4.57 | 0.50 | 5.20 | 83.8 | 16.1 | 2.34 | 2.27 | 1.66 |
| FerretDB v1.105.0 (0c784f2c) + pgrust 0.4-beta (PostgreSQL 18.6 compatible) | **174** | 0.75 | 0.62 | 6.00 | 6.39 | 0.70 | 0.68 | 0.61 | 7.49 | 0.73 | 4.75 | 4.70 | 0.57 | 5.29 | 112 | 15.3 | 2.59 | 2.58 | 1.95 |
| FerretDB v1.105.0 (0db7b801) + TiDB v7.5.1 | **240** | 2.00 | 1.48 | 32.7 | 33.0 | 1.44 | 1.95 | 1.68 | 10.4 | 2.96 | 7.61 | 7.59 | 1.50 | 8.35 | 97.2 | 18.4 | 4.32 | 4.81 | 2.71 |
| FerretDB v1.105.0 (0db7b801) + MariaDB 12.3.3 | **290** | 1.06 | 0.90 | 11.6 | 11.6 | 1.20 | 1.13 | 0.96 | 7.68 | 1.28 | 6.63 | 6.77 | 0.91 | 6.90 | 216 | 9.12 | 2.52 | 2.48 | 1.43 |
| FerretDB v1.105.0 (0db7b801) + Dolt 2.6.2 | **20,470** | 42.6 | 1.87 | 852 | 847 | 2.09 | 188 | 84.7 | 2,876 | 2,846 | 2,857 | 2,836 | 2.06 | 2,834 | 4,126 | 56.6 | 6.94 | 6.92 | 4.75 |
### The slowest parameter
The same, for the slowest of the 15 parameters - nearly always the biggest board or the
busiest user. This is how long the person who waits longest waits. Same row order.
busiest user. This is how long the person who waits longest waits. Ordered by its own
total, so the order can differ from the table above.
| Database | Total | Login | User by id | All Boards count | All Boards page | Board by id | Board lists | Board swimlanes | Board cards | Card window | Card count | Cards per list | Card by id | Card search | Due Cards | People page | Edit card | Move card | Add card |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| MongoDB 9.0.4 | **1,110** | 0.78 | 0.69 | 28.1 | 187 | 0.72 | 1.89 | 1.63 | 297 | 0.83 | 4.48 | 9.99 | 0.68 | 28.1 | 525 | 19.9 | 0.85 | 1.48 | 1.02 |
| MongoDB 6.0.28 | **1,257** | 0.80 | 0.78 | 29.6 | 256 | 0.95 | 1.82 | 1.43 | 300 | 0.98 | 7.77 | 22.4 | 0.78 | 24.9 | 585 | 21.1 | 0.87 | 1.11 | 0.79 |
| MongoDB 7.0.45 | **1,303** | 0.88 | 0.77 | 34.1 | 291 | 0.83 | 1.83 | 1.65 | 304 | 0.89 | 7.95 | 22.1 | 0.77 | 29.6 | 583 | 20.8 | 0.84 | 1.04 | 0.79 |
| MongoDB 8.0.34 | **1,212** | 0.87 | 0.71 | 33.7 | 184 | 0.82 | 2.09 | 1.66 | 301 | 0.93 | 5.14 | 11.0 | 0.73 | 33.8 | 611 | 22.1 | 0.84 | 1.12 | 0.80 |
| FerretDB v1.99.0 (b6c494e9) + SQLite (modernc.org/sqlite v1.61.0) | **29,845** | 1.04 | 0.97 | 4,780 | 4,812 | 1.27 | 18.1 | 8.09 | 1,419 | 389 | 538 | 755 | 0.96 | 504 | 13,798 | 2,804 | 5.69 | 7.52 | 2.83 |
| FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | **136,362** | 1.39 | 1.07 | 190 | 252 | 1.22 | 2.90 | 2.50 | 451 | 113 | 90.2 | 90.2 | 0.98 | 90.0 | 134,900 | 147 | 13.8 | 7.81 | 6.78 |
| FerretDB v1.99.0 (b6c494e9) + MariaDB 12.3.3 | **130,784** | 1.87 | 2.46 | 15,784 | 15,689 | 2.05 | 9.24 | 5.17 | 1,192 | 2.71 | 1,101 | 1,079 | 2.63 | 1,079 | 93,914 | 891 | 11.3 | 10.5 | 7.36 |
| FerretDB v1.99.0 (b6c494e9) + PostgreSQL 18.6 | **127,323** | 1.27 | 1.31 | 887 | 812 | 1.56 | 6.39 | 3.80 | 1,365 | 1.63 | 762 | 874 | 1.20 | 848 | 120,122 | 1,601 | 12.2 | 13.1 | 10.4 |
| FerretDB v1.99.0 (b6c494e9) + MySQL 9.7.4 | **131,734** | 1.98 | 3.56 | 1,821 | 1,815 | 2.41 | 7.84 | 4.79 | 1,561 | 6.74 | 791 | 873 | 1.84 | 936 | 122,154 | 1,709 | 15.7 | 16.6 | 12.5 |
| MongoDB 9.0.2 | **45.1** | 0.59 | 0.47 | 2.54 | 4.03 | 0.48 | 0.50 | 0.50 | 13.9 | 0.45 | 0.64 | 0.90 | 0.37 | 1.87 | 15.6 | 0.71 | 0.42 | 0.59 | 0.54 |
| MongoDB 8.0.32 | **48.8** | 0.52 | 0.44 | 2.63 | 4.09 | 0.42 | 0.59 | 0.55 | 14.6 | 0.44 | 0.66 | 0.91 | 0.40 | 2.08 | 18.4 | 0.83 | 0.40 | 0.45 | 0.37 |
| MongoDB 7.0.43 | **50.1** | 0.51 | 0.46 | 2.41 | 5.25 | 0.44 | 0.54 | 0.48 | 14.3 | 0.42 | 0.64 | 1.26 | 0.40 | 1.83 | 18.7 | 0.80 | 0.52 | 0.60 | 0.54 |
| MongoDB 6.0.28 | **50.2** | 0.48 | 0.52 | 2.49 | 5.59 | 0.49 | 0.52 | 0.50 | 14.6 | 0.42 | 0.66 | 1.33 | 0.39 | 2.05 | 17.6 | 0.87 | 0.54 | 0.55 | 0.59 |
| FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | **154** | 0.74 | 0.59 | 2.40 | 3.22 | 0.88 | 0.91 | 0.65 | 20.4 | 1.65 | 1.43 | 2.17 | 0.57 | 2.74 | 107 | 2.01 | 2.28 | 2.17 | 2.06 |
| FerretDB v1.105.0 (9030e55c) + SQLite (modernc.org/sqlite v1.61.0) | **219** | 0.34 | 0.30 | 21.6 | 11.2 | 0.35 | 0.82 | 0.67 | 44.0 | 3.71 | 13.9 | 20.6 | 0.24 | 5.43 | 85.6 | 6.88 | 1.53 | 1.49 | 0.77 |
| FerretDB v1.105.0 (0db7b801) + MySQL 9.7.2 | **415** | 1.00 | 0.93 | 36.7 | 38.9 | 1.05 | 1.43 | 1.11 | 61.6 | 1.26 | 36.6 | 37.1 | 0.93 | 41.5 | 128 | 18.3 | 3.26 | 3.21 | 2.63 |
| FerretDB v1.105.0 (0c784f2c) + PostgreSQL 18.6 | **483** | 0.87 | 0.73 | 38.3 | 39.1 | 0.68 | 0.97 | 0.79 | 66.7 | 1.22 | 36.7 | 37.0 | 0.57 | 41.4 | 193 | 16.9 | 2.65 | 2.86 | 1.91 |
| FerretDB v1.105.0 (0c784f2c) + pgrust 0.4-beta (PostgreSQL 18.6 compatible) | **594** | 0.88 | 0.67 | 36.5 | 38.8 | 0.80 | 1.13 | 0.92 | 62.5 | 1.19 | 36.4 | 36.1 | 0.60 | 40.3 | 314 | 15.8 | 2.78 | 2.77 | 2.12 |
| FerretDB v1.105.0 (0db7b801) + TiDB v7.5.1 | **877** | 2.12 | 1.52 | 77.1 | 77.7 | 1.58 | 2.64 | 2.11 | 76.4 | 4.03 | 51.1 | 49.9 | 1.62 | 55.6 | 442 | 18.9 | 4.53 | 5.05 | 2.86 |
| FerretDB v1.105.0 (0db7b801) + MariaDB 12.3.3 | **1,063** | 1.19 | 0.98 | 104 | 104 | 1.39 | 1.66 | 1.34 | 54.4 | 1.39 | 49.9 | 50.7 | 0.98 | 51.3 | 622 | 9.26 | 2.84 | 2.63 | 1.61 |
| FerretDB v1.105.0 (0db7b801) + Dolt 2.6.2 | **20,904** | 43.8 | 1.99 | 875 | 887 | 2.36 | 192 | 88.5 | 2,912 | 2,890 | 2,895 | 2,908 | 2.57 | 2,860 | 4,262 | 61.4 | 8.81 | 8.48 | 4.91 |
## Loading the data
Inserting the documents in batches of 1,000, then creating every index WeKan creates.
Ordered by the whole time, fastest first.
| Database | users | boards | lists | swimlanes | cards | Indexes | Index errors |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| MongoDB 9.0.4 | 0.8 s | 3.4 s | 4.5 s | 4.1 s | 17.0 s | 43.6 s | none |
| MongoDB 6.0.28 | 0.8 s | 3.3 s | 4.6 s | 4.2 s | 17.7 s | 41.6 s | none |
| MongoDB 7.0.45 | 0.8 s | 3.3 s | 4.7 s | 4.2 s | 17.3 s | 42.8 s | none |
| MongoDB 8.0.34 | 0.8 s | 3.2 s | 4.9 s | 4.3 s | 17.9 s | 45.3 s | none |
| FerretDB v1.99.0 (b6c494e9) + SQLite (modernc.org/sqlite v1.61.0) | 10.5 s | 65.5 s | 59.6 s | 41.4 s | 315.3 s | 1157.0 s | none |
| FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | 1.5 s | 6.0 s | 7.7 s | 6.9 s | 32.3 s | 219.2 s | none |
| FerretDB v1.99.0 (b6c494e9) + MariaDB 12.3.3 | 8.5 s | 35.9 s | 33.7 s | 28.3 s | 148.9 s | 900.9 s | none |
| FerretDB v1.99.0 (b6c494e9) + PostgreSQL 18.6 | 15.2 s | 83.7 s | 74.6 s | 63.3 s | 376.9 s | 116.8 s | none |
| FerretDB v1.99.0 (b6c494e9) + MySQL 9.7.4 | 11.2 s | 58.6 s | 58.0 s | 53.1 s | 254.2 s | 172.0 s | none |
| MongoDB 9.0.2 | 0.0 s | 0.0 s | 0.1 s | 0.0 s | 0.2 s | 0.6 s | none |
| MongoDB 7.0.43 | 0.0 s | 0.0 s | 0.1 s | 0.0 s | 0.2 s | 0.6 s | none |
| MongoDB 6.0.28 | 0.0 s | 0.0 s | 0.1 s | 0.0 s | 0.2 s | 0.6 s | none |
| MongoDB 8.0.32 | 0.0 s | 0.0 s | 0.1 s | 0.0 s | 0.2 s | 0.7 s | none |
| FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | 0.0 s | 0.1 s | 0.1 s | 0.1 s | 0.3 s | 1.3 s | none |
| FerretDB v1.105.0 (0db7b801) + MySQL 9.7.2 | 0.2 s | 0.5 s | 0.4 s | 0.4 s | 2.0 s | 1.7 s | none |
| FerretDB v1.105.0 (0db7b801) + MariaDB 12.3.3 | 0.1 s | 0.3 s | 0.3 s | 0.2 s | 1.3 s | 7.2 s | none |
| FerretDB v1.105.0 (9030e55c) + SQLite (modernc.org/sqlite v1.61.0) | 0.1 s | 0.5 s | 0.3 s | 0.2 s | 1.9 s | 7.4 s | none |
| FerretDB v1.105.0 (0db7b801) + TiDB v7.5.1 | 0.3 s | 0.6 s | 0.5 s | 0.5 s | 2.2 s | 7.9 s | none |
| FerretDB v1.105.0 (0c784f2c) + PostgreSQL 18.6 | 0.1 s | 0.7 s | 0.5 s | 0.4 s | 3.0 s | 16.0 s | none |
| FerretDB v1.105.0 (0c784f2c) + pgrust 0.4-beta (PostgreSQL 18.6 compatible) | 0.1 s | 0.7 s | 0.5 s | 0.4 s | 3.2 s | 38.0 s | none |
| FerretDB v1.105.0 (0db7b801) + Dolt 2.6.2 | 0.2 s | 0.8 s | 0.8 s | 0.6 s | 4.2 s | 105.7 s | none |
## Same data
Every answer of every query, for every parameter, and the cards the writes left behind,
compared with **MongoDB 8.0.34**. Answers are compared as typed values with
compared with **MongoDB 8.0.32**. Answers are compared as typed values with
sorted keys; a query whose order WeKan relies on must also return the same order.
| Database | Same answers | Different |
| --- | --- | --- |
| MongoDB 9.0.4 | 19 of 19 | none |
| MongoDB 9.0.2 | 19 of 19 | none |
| MongoDB 8.0.32 | 19 of 19 | none |
| MongoDB 6.0.28 | 19 of 19 | none |
| MongoDB 7.0.45 | 19 of 19 | none |
| MongoDB 8.0.34 | 19 of 19 | none |
| FerretDB v1.99.0 (b6c494e9) + SQLite (modernc.org/sqlite v1.61.0) | 19 of 19 | none |
| MongoDB 7.0.43 | 19 of 19 | none |
| FerretDB v2.7.0 + PostgreSQL 17.6 with DocumentDB | 19 of 19 | none |
| FerretDB v1.99.0 (b6c494e9) + MariaDB 12.3.3 | 19 of 19 | none |
| FerretDB v1.99.0 (b6c494e9) + PostgreSQL 18.6 | 19 of 19 | none |
| FerretDB v1.99.0 (b6c494e9) + MySQL 9.7.4 | 19 of 19 | none |
| FerretDB v1.105.0 (9030e55c) + SQLite (modernc.org/sqlite v1.61.0) | 19 of 19 | none |
| FerretDB v1.105.0 (0db7b801) + MySQL 9.7.2 | 19 of 19 | none |
| FerretDB v1.105.0 (0c784f2c) + PostgreSQL 18.6 | 19 of 19 | none |
| FerretDB v1.105.0 (0c784f2c) + pgrust 0.4-beta (PostgreSQL 18.6 compatible) | 19 of 19 | none |
| FerretDB v1.105.0 (0db7b801) + TiDB v7.5.1 | 19 of 19 | none |
| FerretDB v1.105.0 (0db7b801) + MariaDB 12.3.3 | 19 of 19 | none |
| FerretDB v1.105.0 (0db7b801) + Dolt 2.6.2 | 19 of 19 | none |
## The queries
@@ -103,7 +139,7 @@ Each is the query WeKan itself sends; see [queries.cjs](queries.cjs).
| Add card | cards.insertOne({ ...a new card }) | the 3 biggest boards + 12 others |
Each query is first run once per parameter to warm up (those answers are the ones compared),
then repeated for up to 5 passes or 10 seconds; 15-90 calls per query were timed.
then repeated for up to 5 passes or the time budget (WEKAN_SPEEDTEST_BUDGET_MS); 15-90 calls per query were timed.
A query too slow to be repeated in that time keeps its warm-up time.
Raw results: `.tools/log/test-db-speedtest/2026-10-23_19-33-27/speedtest-<database>.json`.
Raw results: `.tools/log/test-db-speedtest/2026-11-03_22-10-05/speedtest-<database>.json`.
@@ -0,0 +1,108 @@
# Opening a WeKan board: where the time goes
The [speed test](../) asks the database alone. This asks what a person waits for: the
whole of opening a board in WeKan, with WeKan on FerretDB (SQLite) and on MongoDB 8,
and how much of it is the database. [board-open.cjs](board-open.cjs) seeds a board of
N cards, logs in, opens it in Chromium and records
- the wall clock to the first minicard and until the DDP WebSocket has been quiet for
1.5 s (**settled**), and what crossed the WebSocket;
- the Meteor server's CPU, by who spends it (a Node inspector profile of the server);
- FerretDB's CPU (its pprof), against what it spends with nobody in the browser.
```sh
# WeKan on FerretDB: MONGO_URL=mongodb://127.0.0.1:27040/wekan?directConnection=true,
# DEFAULT_METEOR_REACTIVITY_ORDER=polling (as WeKan is deployed), meteor run --inspect=9229,
# and FerretDB with --debug-addr=127.0.0.1:8098
WEKAN_BASE_URL=http://localhost:3310 WEKAN_MONGO_URL=mongodb://127.0.0.1:27040/wekan \
node docs/Databases/FerretDB/1/SpeedTest/app/board-open.cjs --sizes 200,2000,5000 \
--inspect 9229 --ferret-pprof http://127.0.0.1:8098 --label ferretdb-sqlite
```
Run on 2026-11-03/04, Apple M2 Max, **the development build of Meteor** (so the browser's
own part is larger than in production), one user, `polling` reactivity, FerretDB v1.105.0
with the SQLite membership-test change, MongoDB 8.0 in Docker. Each board is opened
twice (the second is warm), and the whole measurement was run **twice** (run 1 / run 2
below), because one run is not enough: the same board's "settled" time moved by up to
300 ms between runs.
## Result: the database is not what you wait for on a small board, and is on a big one
Milliseconds, first open of a board with this many cards (run 1 / run 2). WeKan loads
cards lazily, so the browser gets **the same ~210 KB and draws 70-120 minicards at every
size**.
| Cards | FerretDB (SQLite): first card | settled | MongoDB 8: first card | settled |
| ---: | ---: | ---: | ---: | ---: |
| 200 | 750 / 764 | 966 / 880 | 752 / 698 | 865 / 813 |
| 2,000 | 931 / 1,031 | 1,763 / 1,555 | 829 / 856 | 1,457 / 1,582 |
| 5,000 | 1,331 / 1,334 | 2,166 / 2,065 | 938 / 899 | 1,562 / 1,622 |
What is the same in both runs:
- **5,000 cards: FerretDB is 400-600 ms slower**, to the first card by 42-48% and until
settled by 27-39%.
- **2,000 cards: within the noise** for "settled" (-30 to +300 ms), and 100-175 ms
slower to the first card.
- **200 cards: 70-100 ms**, which is 9-12%.
The warm second open of run 2: 5,000 cards 1,321 against 865 ms to the first card
and 1,946 against 1,594 ms settled; 2,000 cards 904 against 824 and 1,628 against
1,446; 200 cards 721 against 689 and 934 against 805.
The Meteor server's CPU is the same on both databases within the noise (first open, run
1 / run 2: 102 / 119, 276 / 421 and 470 / 517 ms on FerretDB; 178 / 93, 245 / 342 and
521 / 483 ms on MongoDB), so the difference is **time spent waiting for the database**,
and it grows with the size of the board while the data the browser receives does not.
For a board of a few hundred cards, 0.7-0.75 s to the first card is nearly the same on
both: that is the application (the development bundle, Blaze, the DDP round trips), not
the database.
## Where the Meteor server spends its CPU (5,000 cards, first open, run 2)
| | FerretDB (517 ms) | MongoDB 8 (483 ms) |
| --- | ---: | ---: |
| Garbage collector | 31% | 24% |
| MongoDB driver (wire, decode, encode) | 19% | 22% |
| Meteor mongo (observers, polling, diff) | 12% | 13% |
| Node internals | 11% | 12% |
| WeKan code | 11% | 10% |
| Other Meteor packages and dependencies | 8% | 9% |
| Native, EJSON, DDP server (publish, mergebox) | 8% | 10% |
The DDP server itself, publish and mergebox, is 1%. The largest single functions are
the garbage collector, `CollectionDocument` (WeKan's model class), `replaceTypes`
(Meteor's BSON-to-EJSON step) and the BSON decoder.
## Where FerretDB spends its CPU
With nobody in the browser FerretDB uses 110-510 ms of CPU in the 14 s window the
profile covers. Opening a board adds, net: about nothing at 200 cards, **about 1.6 s at
2,000 and about 3.9-4.1 s at 5,000** - eight times the Meteor server's CPU for the same
open. For the 5,000-card board, of 4.41 s (run 2):
- the SQLite engine, 1.72 s (39%), a good part of it moving through the pages of the
cards table (`btreeInitPage` 18% by itself);
- 34% is `filterIterator.Next`: documents read from SQLite and filtered again in Go;
- by MongoDB command: `find` 0.79 s, `getMore` 0.74 s, `aggregate` 0.68 s (a `$group`
done in Go over the documents it read);
- the Go garbage collector, 0.57 s (13%).
Which WeKan query sends the `aggregate` during the open was **not identified**; the
debug log of a 2,000-card open contains none, so the 5,000-card profile may have
included the All Boards page's card counters (`getAllBoardsTileData`, a `$group` over
the user's boards) that the login lands on first. Treat that line as a lead.
## What this does and does not say
- It says the database layer is the part of opening a board that **scales with the board
size**, and that on FerretDB (SQLite) a 5,000-card board takes 400-600 ms longer than on
MongoDB, with the same data sent to the browser.
- It says the **Meteor server is not the bottleneck**: 0.1-0.5 s of CPU, the same on both
databases, and the DDP server is about 1% of it. A backend rewritten in another language
could save a part of that, and would not change the database or the browser.
- It does not separate the browser's own time from the wait for the server: the first
card at 0.7 s on a small board is mostly the development bundle and rendering.
- Single machine, the development build, one user, two runs, two opens per board, `settled`
meaning 1.5 s without a DDP frame. A production build and a loaded server would differ.
@@ -0,0 +1,188 @@
'use strict';
// Where the time goes when a WeKan board is opened, with WeKan running on a database
// (FerretDB on SQLite, or MongoDB): the browser's wall clock, what crosses the DDP
// WebSocket, the Meteor server's CPU by who spends it (Node inspector profile), and
// FerretDB's CPU (its pprof). The speed test asks the database alone; this asks whether
// the database is what a person waits for.
//
// # WeKan on FerretDB: meteor run --port 3310 --inspect=9229 with MONGO_URL set,
// # FerretDB with --debug-addr=127.0.0.1:8098
// WEKAN_BASE_URL=http://localhost:3310 WEKAN_MONGO_URL=mongodb://127.0.0.1:27040/wekan \
// node docs/Databases/FerretDB/1/SpeedTest/app/board-open.cjs --sizes 200,2000 \
// --inspect 9229 --ferret-pprof http://127.0.0.1:8098 --label ferretdb-sqlite
//
// Dev servers: the Meteor build is the development one, so the client's own time is
// larger than in production; the comparison between databases is what it is for.
const path = require('path');
const { execFileSync } = require('child_process');
const ROOT = path.join(__dirname, '../../../../../..');
const pw = require(path.join(ROOT, 'tests/playwright/node_modules/playwright-core'));
const { MongoClient } = require(path.join(ROOT, 'tests/playwright/node_modules/mongodb'));
const WebSocket = require(path.join(ROOT, 'node_modules/ws'));
function arg(name, fallback) {
const i = process.argv.indexOf(`--${name}`);
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
}
const BASE = process.env.WEKAN_BASE_URL || 'http://localhost:3310';
process.env.WEKAN_BASE_URL = BASE;
const MONGO = process.env.WEKAN_MONGO_URL || 'mongodb://127.0.0.1:27040/wekan';
process.env.WEKAN_MONGO_URL = MONGO;
const db = require(path.join(ROOT, 'tests/playwright/helpers/db.js'));
const { loginWithToken } = require(path.join(ROOT, 'tests/playwright/helpers/auth.js'));
const SIZES = arg('sizes', '200,2000').split(',').map(Number);
const INSPECT = arg('inspect', '');
const PPROF = arg('ferret-pprof', '');
const LABEL = arg('label', 'run');
const GO = arg('go', path.join(ROOT, '.tools/go/bin/go'));
const FERRET_BIN = arg('ferret-bin', path.join(ROOT, '.tools/w/FerretDB/bin/ferretdb'));
const QUIET_MS = 1500;
// ── seeding ─────────────────────────────────────────────────────────────────
async function seedBoard(user, size) {
const board = db.seedBoard({ ownerId: user.id, title: `Open ${size}`, listCount: 6, cardTitlesPerList: [['Template']] });
const template = db.findOne('cards', { boardId: board.boardId, title: 'Template' });
const client = await new MongoClient(MONGO).connect();
try {
const cards = client.db().collection('cards');
const docs = [];
for (let i = 0; i < size; i += 1) {
docs.push({
...template,
_id: `card${board.boardId}x${i}`,
title: `Card ${i} ${['plan', 'build', 'review', 'ship'][i % 4]} the thing`,
listId: board.listIds[i % board.listIds.length],
sort: Math.floor(i / board.listIds.length) * 100,
cardNumber: i + 2,
createdAt: new Date(), modifiedAt: new Date(), dateLastActivity: new Date(),
});
}
for (let i = 0; i < docs.length; i += 1000) await cards.insertMany(docs.slice(i, i + 1000));
} finally {
await client.close();
}
return board;
}
// ── the Meteor server's CPU, from the Node inspector ────────────────────────
async function inspector() {
const list = await (await fetch(`http://127.0.0.1:${INSPECT}/json`)).json();
const ws = new WebSocket(list[0].webSocketDebuggerUrl);
await new Promise((resolve, reject) => { ws.on('open', resolve); ws.on('error', reject); });
let id = 0;
const pending = new Map();
ws.on('message', (m) => {
const msg = JSON.parse(m);
if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); }
});
const send = (method, params = {}) => new Promise((resolve) => { id += 1; pending.set(id, resolve); ws.send(JSON.stringify({ id, method, params })); });
await send('Profiler.enable');
await send('Profiler.setSamplingInterval', { interval: 250 });
return {
start: () => send('Profiler.start'),
stop: async () => (await send('Profiler.stop')).result.profile,
close: () => ws.close(),
};
}
const { analyse } = require('./profileAnalysis.cjs');
// ── FerretDB's CPU, from its pprof ──────────────────────────────────────────
async function ferretProfile(seconds) {
if (!PPROF) return null;
const res = await fetch(`${PPROF}/debug/pprof/profile?seconds=${seconds}`);
const file = path.join(process.env.TMPDIR || '/tmp', `ferret-open-${Date.now()}.prof`);
require('fs').writeFileSync(file, Buffer.from(await res.arrayBuffer()));
const env = { ...process.env, HOME: path.join(ROOT, '.tools') };
const top = execFileSync(GO, ['tool', 'pprof', '-top', '-nodecount=80', FERRET_BIN, file], { encoding: 'utf8', env });
const total = /Total samples = ([\d.]+)(m?s)/.exec(top);
const cpuMs = total ? Number(total[1]) * (total[2] === 's' ? 1000 : 1) : null;
// A line reads: flat flat% sum% cum cum% name - SQLite's share is the cumulative time of its VM.
const line = top.split('\n').find((l) => /_sqlite3VdbeExec/.test(l));
const sqlite = line ? /([\d.]+)(m?s)\s+[\d.]+%\s+[\d.]+%\s+([\d.]+)(m?s)\s+[\d.]+%/.exec(line) : null;
// The cumulative top, for the first open: who in FerretDB spends it.
const cum = execFileSync(GO, ['tool', 'pprof', '-top', '-cum', '-nodecount=34', FERRET_BIN, file], { encoding: 'utf8', env })
.split('\n').filter((l) => /^\s+[\d.]+m?s/.test(l)).map((l) => l.trim().replace(/\s+/g, ' ')).slice(0, 34);
return { cum, cpuMs, sqliteMs: sqlite ? Number(sqlite[3]) * (sqlite[4] === 's' ? 1000 : 1) : null };
}
// ── one board open ──────────────────────────────────────────────────────────
async function openBoard(user, board, size) {
const browser = await pw.chromium.launch();
const context = await browser.newContext({ viewport: { width: 1500, height: 1000 } });
const page = await context.newPage();
await page.addInitScript(() => { try { localStorage.setItem('wekan-mobile-mode', 'false'); } catch (e) { /* none */ } });
let frames = 0; let bytes = 0; let last = Date.now();
page.on('websocket', (ws) => ws.on('framereceived', (e) => { frames += 1; bytes += e.payload.length; last = Date.now(); }));
await loginWithToken(page, user.id, user.token);
await page.waitForTimeout(1500);
const insp = INSPECT ? await inspector() : null;
frames = 0; bytes = 0;
const ferret = PPROF ? ferretProfile(14) : Promise.resolve(null);
if (insp) await insp.start();
const t0 = Date.now();
await page.goto(`${BASE}/b/${board.boardId}/${board.slug}`, { waitUntil: 'commit' });
let tFirst = null;
await page.waitForSelector('.minicard', { timeout: 60000 }).then(() => { tFirst = Date.now() - t0; });
// quiet: no DDP frame for QUIET_MS
for (;;) {
if (Date.now() - last > QUIET_MS && Date.now() - t0 > (tFirst || 0)) break;
if (Date.now() - t0 > 60000) break;
await page.waitForTimeout(100);
}
const tQuiet = Date.now() - t0 - QUIET_MS;
const profile = insp ? await insp.stop() : null;
const minicards = await page.locator('.minicard').count();
const ferretResult = await ferret;
if (insp) insp.close();
await browser.close();
return { size, tFirst, tQuiet, frames, kb: Math.round(bytes / 1024), minicards, server: profile ? analyse(profile) : null, ferret: ferretResult };
}
const f = (v) => (v === null || v === undefined ? '-' : Math.round(v).toLocaleString('en-US'));
async function main() {
const user = db.seedUser();
const results = [];
// What FerretDB spends in the same 14 s with nobody in the browser: the baseline.
const idle = PPROF ? await ferretProfile(14) : null;
if (idle) console.error(`FerretDB idle for 14 s: ${f(idle.cpuMs)} ms of CPU`);
for (const size of SIZES) {
console.error(`seeding a board of ${size} cards...`);
const board = await seedBoard(user, size);
// The first open of a board is the one a person waits for; a second shows a warm server.
for (const pass of ['first open', 'second open']) {
console.error(`opening ${size} (${pass})...`);
const r = await openBoard(user, board, size);
r.pass = pass;
results.push(r);
}
}
console.log(`\n## ${LABEL}: opening a board\n`);
if (idle) console.log(`FerretDB with nobody in the browser: ${f(idle.cpuMs)} ms of CPU in the same 14 s window (subtract it).\n`);
console.log('| Cards | Open | First card (ms) | Settled (ms) | DDP frames | DDP KB | Minicards drawn | Meteor server busy (ms) | FerretDB CPU (ms) | of it SQLite (ms) |');
console.log('| ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |');
for (const r of results) {
console.log(`| ${r.size} | ${r.pass} | ${f(r.tFirst)} | ${f(r.tQuiet)} | ${f(r.frames)} | ${f(r.kb)} | ${f(r.minicards)} | ${f(r.server && r.server.busyMs)} | ${f(r.ferret && r.ferret.cpuMs)} | ${f(r.ferret && r.ferret.sqliteMs)} |`);
}
for (const r of results.filter((x) => x.pass === 'first open' && x.server)) {
console.log(`\n### The Meteor server, ${r.size} cards, first open: ${f(r.server.busyMs)} ms of CPU\n`);
for (const [c, ms] of r.server.categories) console.log(`- ${c}: ${f(ms)} ms (${Math.round((100 * ms) / r.server.busyMs)}%)`);
if (r.ferret && r.ferret.cum) {
console.log(`\n### FerretDB, ${r.size} cards, first open: cumulative CPU by function (flat flat% sum% cum cum% name)\n`);
console.log('```');
r.ferret.cum.forEach((l) => console.log(l));
console.log('```');
}
console.log('\nTop functions:');
for (const [fn, ms] of r.server.topFunctions) console.log(`- ${f(ms)} ms ${fn}`);
}
process.exit(0);
}
if (require.main === module) main().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,46 @@
'use strict';
// A Node inspector CPU profile of the Meteor server, by who spends the time. Apart from
// the browser and the database so a test can drive it with a made-up profile.
function categorise(node) {
const { functionName, url } = node.callFrame;
if (functionName === '(idle)') return 'idle';
if (functionName === '(garbage collector)') return 'garbage collector';
if (functionName === '(program)') return 'native (program)';
if (/\/mongodb\/|npm-mongo|\/bson\//.test(url)) return 'MongoDB driver (wire, decode, encode)';
if (/packages\/ddp-server|packages\/ddp-common|packages\/ddp\b/.test(url)) return 'Meteor DDP (publish, mergebox, sessions)';
if (/packages\/mongo|minimongo|diff-sequence/.test(url)) return 'Meteor mongo (observe, polling, diff)';
if (/packages\/ejson/.test(url)) return 'EJSON';
// Other Meteor packages and dependencies are not WeKan's: their paths contain /server/ too.
if (/programs\/server\/packages|node_modules/.test(url)) return 'other packages';
if (/programs\/server\/app|\/_build\/|\/models\/|\/server\/|\/imports\/|wekan/.test(url)) return 'WeKan code';
if (/^node:/.test(url) || url === '') return 'node internals';
return 'other packages';
}
function analyse(profile) {
const byId = new Map(profile.nodes.map((n) => [n.id, n]));
const cats = new Map();
const fns = new Map();
let total = 0;
profile.samples.forEach((sid, i) => {
const dt = (profile.timeDeltas[i + 1] ?? profile.timeDeltas[i] ?? 0) / 1000; // ms
const node = byId.get(sid);
const cat = categorise(node);
cats.set(cat, (cats.get(cat) || 0) + dt);
total += dt;
if (cat !== 'idle') {
const key = `${node.callFrame.functionName || '(anonymous)'} ${node.callFrame.url.split('/').slice(-2).join('/')}`;
fns.set(key, (fns.get(key) || 0) + dt);
}
});
const idle = cats.get('idle') || 0;
return {
totalMs: total, busyMs: total - idle,
categories: [...cats.entries()].filter(([c]) => c !== 'idle').sort((a, b) => b[1] - a[1]),
topFunctions: [...fns.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8),
};
}
module.exports = { categorise, analyse };
@@ -0,0 +1,148 @@
# Go databases that could be linked into FerretDB, against its SQLite
FerretDB is one Go binary, and its SQLite handler is the pure-Go driver
[modernc.org/sqlite](https://gitlab.com/cznic/sqlite) inside it. Is any database
implemented in Go (the [awesome-go list](https://github.com/avelino/awesome-go#databases-implemented-in-go))
one that could be linked in beside it, has enough to take SQLite's place, and is
faster for what WeKan asks?
**Short answer: no. Nothing is faster in a way that matters, and the ones that are
faster at a single read or write are key-value stores that would make the handler
do SQLite's job.**
This is the engine level of the question, so it can be answered in minutes and
without writing a handler for each: [bench.go](main.go) loads the speed test's cards
and asks each engine the questions WeKan asks of cards, the way a FerretDB handler
written for it would ask them.
```sh
node docs/Databases/FerretDB/1/SpeedTest/embedded/export.cjs 0.05 <dir> # the speed test's cards as JSON lines
cd docs/Databases/FerretDB/1/SpeedTest/embedded && go run . -data <dir> # go 1.24+, pure Go, no cgo
```
Run on 2026-11-03 on an Apple M2 Max, Go 1.27, one engine at a time, each in its own
directory. `-only "Pebble (CockroachDB's LSM),Nutsdb"` runs some.
## What is asked
| Column | The question |
| --- | --- |
| By id | one card by `_id` |
| Board cards | the unarchived cards of a board (an index on boardId and archived), **decoded** from JSON as FerretDB must |
| (raw, not decoded) | the same fetch without the decoding: the part that differs between engines |
| Count | how many unarchived cards a board has |
| Edit, Move | read the card, change it in Go, write it back (FerretDB's `updateOne`), 200 different cards |
| Add | insert a card |
Each read is asked of 15 boards - the 3 biggest and 12 ordinary ones, as in the
speed test - and a cell is the average of the 15 medians. Times are milliseconds
for one call, **Total** is the sum of the six and the table is ordered by it. Every
engine first has to give the right number of cards and the right counts for every
board, and the writes must be there afterwards: a fast wrong answer is no answer.
**SQLite is set up as FerretDB sets it**: WAL, `synchronous=NORMAL`, a 64 MiB cache
and 256 MiB of mmap, the document as JSON and the indexes on `json_extract`
expressions. A row with plain columns shows what SQLite could do and FerretDB does not.
The key-value stores keep a document under `d/<id>` and an index of
(boardId, archived) as keys, which is what a handler on them would have to build.
No engine fsyncs per commit except where the row says so, to match SQLite's
`NORMAL`.
## 15,377 cards (scale 0.05)
| # | Engine | Total | By id | Board cards | (raw, not decoded) | Count | Edit | Move | Add | Load (s) | Size (MB) |
| ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | Nutsdb | **1.394** | 0.0004 | 1.332 | 0.057 | 0.0066 | 0.019 | 0.032 | 0.0041 | 0.0 | 13 |
| 2 | bbolt, no fsync | **1.481** | 0.0005 | 1.349 | 0.055 | 0.0047 | 0.037 | 0.054 | 0.035 | 0.2 | 27 |
| 3 | SQLite, plain columns (not what FerretDB does) | **1.598** | 0.0067 | 1.385 | 0.116 | 0.025 | 0.057 | 0.067 | 0.058 | 0.1 | 15 |
| 4 | Badger | **1.623** | 0.0015 | 1.536 | 0.152 | 0.012 | 0.027 | 0.040 | 0.0065 | 0.0 | n/a |
| 5 | SQLite as FerretDB uses it (JSON, expression indexes) | **1.677** | 0.0078 | 1.421 | 0.113 | 0.037 | 0.073 | 0.082 | 0.057 | 0.2 | 14 |
| 6 | SQLite as FerretDB uses it, synchronous=FULL | **1.747** | 0.0079 | 1.421 | 0.118 | 0.037 | 0.098 | 0.099 | 0.084 | 0.2 | 14 |
| 7 | BuntDB (in memory with an append-only file) | **1.795** | 0.0003 | 1.470 | 0.186 | 0.166 | 0.066 | 0.072 | 0.021 | 0.3 | 12 |
| 8 | Pebble (CockroachDB's LSM) | **1.841** | 0.0028 | 1.770 | 0.298 | 0.011 | 0.021 | 0.034 | 0.0010 | 0.1 | 16 |
| 9 | bbolt, fsync on every commit (its default) | **25.5** | 0.0005 | 1.370 | 0.057 | 0.0048 | 8.062 | 8.025 | 8.015 | 0.2 | 27 |
| 10 | Clover (document database over Badger) | **145** | 0.016 | 59.1 | 58.5 | 57.2 | 9.412 | 9.635 | 9.751 | 0.7 | 41 |
## 153,768 cards (scale 0.5)
| # | Engine | Total | By id | Board cards | (raw, not decoded) | Count | Edit | Move | Add | Load (s) | Size (MB) |
| ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | SQLite, plain columns (not what FerretDB does) | **5.950** | 0.0071 | 5.693 | 0.418 | 0.062 | 0.052 | 0.072 | 0.064 | 1.1 | 147 |
| 2 | Nutsdb | **6.007** | 0.0005 | 5.923 | 0.326 | 0.024 | 0.022 | 0.033 | 0.0044 | 0.3 | 132 |
| 3 | bbolt, no fsync | **6.065** | 0.0005 | 5.909 | 0.289 | 0.019 | 0.042 | 0.056 | 0.039 | 14.5 | 274 |
| 4 | SQLite as FerretDB uses it, synchronous=FULL | **6.118** | 0.0089 | 5.726 | 0.413 | 0.103 | 0.098 | 0.097 | 0.085 | 1.8 | 141 |
| 5 | SQLite as FerretDB uses it (JSON, expression indexes) | **6.153** | 0.0082 | 5.830 | 0.430 | 0.103 | 0.072 | 0.079 | 0.062 | 1.8 | 141 |
| 6 | BuntDB (in memory with an append-only file) | **7.001** | 0.0003 | 6.130 | 0.753 | 0.682 | 0.080 | 0.083 | 0.026 | 4.3 | 124 |
| 7 | Badger | **7.419** | 0.0025 | 7.273 | 1.483 | 0.060 | 0.032 | 0.042 | 0.0096 | 0.4 | n/a |
| 8 | Pebble (CockroachDB's LSM) | **10.5** | 0.023 | 10.3 | 4.992 | 0.056 | 0.033 | 0.032 | 0.0012 | 2.0 | 67 |
| 9 | bbolt, fsync on every commit (its default) | **42.0** | 0.0006 | 5.923 | 0.296 | 0.019 | 11.8 | 12.2 | 12.1 | 14.4 | 274 |
| 10 | Clover (document database over Badger) | **1176** | 0.017 | 577 | 571 | 563 | 11.9 | 11.7 | 11.8 | 25.7 | 301 |
## What it says
- **Nothing is faster where it counts.** A board's cards cost 1.4 ms at 15,000 cards
and 5.7-6.2 ms at 154,000 for every engine that is not slower, and nearly all of
it is decoding the JSON, which is the same work whatever stores it. Totals are
within 5% of SQLite's for Nutsdb and bbolt.
- **The key-value stores win single reads and writes, by microseconds.** A card by
id takes 0.0003-0.0006 ms on bbolt, Nutsdb and BuntDB against SQLite's 0.008; an insert
0.001 ms on Pebble against 0.062; an edit 0.022 ms on Nutsdb against 0.072. But the
cheapest call through FerretDB costs about 0.2 ms in the speed test (the protocol,
the handler, the network) and most cost more, so an engine that took nothing at all would make one call
at most a few percent faster.
- **Some are slower, and by a lot.** Pebble's LSM is 12x SQLite on fetching a big
board's cards (5.0 ms against 0.43), Badger 3.5x, Clover about 1,300x (it decodes every
document to answer anything), 90-190x in total. **bbolt with its default - an fsync on every commit
- takes 8-12 ms per write, 100-160x SQLite's**; without it, it is as fast as
SQLite and not durable the same way.
- **The cost is in what is not there.** A key-value store has keys. FerretDB's SQLite
handler is about 8,500 lines because filters, sorting, `$or`, `$in`, regular
expressions, aggregation, indexes and statistics are SQL there. On a key-value
store the handler would do them in Go, and this benchmark asked only the six
easiest questions. Of the stores here only BuntDB and Clover have indexes on JSON
fields, and neither has a query language that could take SQLite's place.
- **Loading** at 154,000 cards: Badger 0.4 s, Nutsdb 0.3 s, Pebble 2.0 s, SQLite 1.8
s, BuntDB 4.3 s, bbolt 14.5 s (unsorted inserts), Clover 26 s.
- Disk: SQLite 141 MB, Pebble 67, Nutsdb 132, BuntDB 124, bbolt 274, Clover 301.
## The list, entry by entry
Tested (Go library, open source, maintained, pure Go):
| Entry | Licence, last push | Result |
| --- | --- | --- |
| [go-sqlite](https://gitlab.com/cznic/sqlite) | BSD-3, FerretDB's own | the baseline, three ways |
| [Pebble](https://github.com/cockroachdb/pebble) | BSD-3, 2026-11 | fastest insert and edit, 12x slower board fetch |
| [Badger](https://github.com/dgraph-io/badger) | Apache-2.0, 2026-10 | fast writes, 3.5x slower board fetch |
| [bbolt](https://github.com/etcd-io/bbolt) | MIT, 2026-10 | as fast as SQLite without fsync, 100x slower writes with it |
| [BuntDB](https://github.com/tidwall/buntdb) | MIT, 2026-05 | fastest by id, slowest count (index scan), in memory |
| [Nutsdb](https://github.com/nutsdb/nutsdb) | Apache-2.0, 2026-10 | on par with SQLite, faster point operations |
| [Clover](https://github.com/ostafen/clover) | MIT, 2025-09 | has indexes and queries, 90-190x slower in total |
Not tested, and why:
| Entry | Why not |
| --- | --- |
| goleveldb (2024-05), tiedot (2021), pudge (2021), scribble (2023), ledisdb (2023), hare, diskv, libradb | not maintained since 2021-2024, or toys; the engines that matter of their kind (LSM, B+tree) are Pebble, Badger and bbolt, tested |
| Bitcask, pogreb, moss, LotusDB, RoseDB, Coffer, tempdb | more key-value stores of the same kind; they would also need the handler to do what SQL does |
| gedb, [minisql](https://github.com/RichardKnop/minisql) (54 stars) | not looked at beyond their descriptions: a MongoDB-like store and a young single-file SQL database |
| [rqlite](https://github.com/rqlite/rqlite), [redka](https://github.com/nalgeon/redka) | they are SQLite underneath, so not faster by construction |
| [Dolt](https://github.com/dolthub/dolt), [TiDB](https://github.com/pingcap/tidb) | tested as servers in the [speed test](../) (Dolt the slowest of all, TiDB 2.3x MySQL's total); embedding their engines (go-mysql-server) would bring the same engines and far more code |
| CockroachDB | not open source (proprietary licence) |
| immudb | GitHub does not recognise its licence as an open-source one (`NOASSERTION`); an immutable ledger |
| DiceDB, godis, rotom, Vasto, ledisdb | Redis-style servers, not libraries |
| Dgraph, EliasDB, NornicDB, MemHop | graph databases |
| InfluxDB, Prometheus, VictoriaMetrics, LinDB, lynxdb, nanotdb, unitdb | time series and logs |
| Milvus | vector database |
| column | in-memory columnar store |
| objectbox-go | needs cgo |
| CovenantSQL, Databunker, NoKV, piladb | blockchain, a PII vault, a metadata service, a stack |
## What would be needed to know more
A real answer for one engine means a handler for it: query pushdown, updates,
indexes, statistics, 2,000-8,000 lines each. This benchmark says where it could pay:
nowhere on reads (decoding dominates), and only on small writes, which the protocol
hides. It does not measure what SQLite does in the engine and a key-value store
would do in Go - sorting, `$or`, regular expressions, aggregation - where SQLite can
only be ahead.
@@ -0,0 +1,120 @@
package main
import (
"github.com/dgraph-io/badger/v4"
)
type badgerEng struct {
db *badger.DB
dir string
}
func openBadger() maker {
return maker{name: "Badger", open: func(dir string) (Engine, error) {
db, err := badger.Open(badger.DefaultOptions(dir).WithLogger(nil).WithSyncWrites(false))
if err != nil {
return nil, err
}
return &badgerEng{db: db, dir: dir}, nil
}}
}
func (e *badgerEng) Name() string { return "Badger" }
func (e *badgerEng) Load(cards []Card) error {
wb := e.db.NewWriteBatch()
defer wb.Cancel()
for _, c := range cards {
if err := wb.Set(docKey(c.ID), c.Raw); err != nil {
return err
}
if err := wb.Set(indexKey(c), nil); err != nil {
return err
}
}
return wb.Flush()
}
func (e *badgerEng) ByID(id string) ([]byte, error) {
var out []byte
err := e.db.View(func(tx *badger.Txn) error {
it, err := tx.Get(docKey(id))
if err != nil {
return err
}
out, err = it.ValueCopy(nil)
return err
})
return out, err
}
func (e *badgerEng) BoardCards(board string) ([][]byte, error) {
var out [][]byte
err := e.db.View(func(tx *badger.Txn) error {
p := boardPrefix(board)
it := tx.NewIterator(badger.IteratorOptions{Prefix: p})
defer it.Close()
var ids []string
for it.Rewind(); it.Valid(); it.Next() {
ids = append(ids, idFromIndexKey(it.Item().Key()))
}
for _, id := range ids {
item, err := tx.Get(docKey(id))
if err != nil {
return err
}
v, err := item.ValueCopy(nil)
if err != nil {
return err
}
out = append(out, v)
}
return nil
})
return out, err
}
func (e *badgerEng) CountBoard(board string) (int, error) {
n := 0
err := e.db.View(func(tx *badger.Txn) error {
it := tx.NewIterator(badger.IteratorOptions{Prefix: boardPrefix(board)})
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
n++
}
return nil
})
return n, err
}
func (e *badgerEng) Update(id string, change func([]byte) ([]byte, error)) error {
return e.db.Update(func(tx *badger.Txn) error {
item, err := tx.Get(docKey(id))
if err != nil {
return err
}
d, err := item.ValueCopy(nil)
if err != nil {
return err
}
next, err := change(d)
if err != nil {
return err
}
return tx.Set(docKey(id), next)
})
}
func (e *badgerEng) Insert(c Card) error {
return e.db.Update(func(tx *badger.Txn) error {
if err := tx.Set(docKey(c.ID), c.Raw); err != nil {
return err
}
return tx.Set(indexKey(c), nil)
})
}
// SizeBytes is -1: Badger keeps a recent write in its memtable and preallocates its
// log files, so the directory says nothing about what the data takes.
func (e *badgerEng) SizeBytes() int64 { return -1 }
func (e *badgerEng) Close() error { return e.db.Close() }
@@ -0,0 +1,120 @@
package main
import (
"bytes"
"fmt"
"path/filepath"
bolt "go.etcd.io/bbolt"
)
type boltEng struct {
db *bolt.DB
dir string
}
// bbolt fsyncs on every commit, which the others here do not; "NoSync" is the same
// engine without it, for a like-for-like with SQLite's synchronous=NORMAL.
func openBolt(name string, noSync bool) maker {
return maker{name: name, open: func(dir string) (Engine, error) {
db, err := bolt.Open(filepath.Join(dir, "cards.bolt"), 0o600, &bolt.Options{NoSync: noSync})
if err != nil {
return nil, err
}
db.NoSync = noSync
return &boltEng{db: db, dir: dir}, nil
}}
}
func (e *boltEng) Name() string { return "bbolt" }
func (e *boltEng) Load(cards []Card) error {
return e.db.Update(func(tx *bolt.Tx) error {
d, err := tx.CreateBucketIfNotExists([]byte("d"))
if err != nil {
return err
}
b, err := tx.CreateBucketIfNotExists([]byte("b"))
if err != nil {
return err
}
for _, c := range cards {
if err := d.Put([]byte(c.ID), c.Raw); err != nil {
return err
}
if err := b.Put(indexKey(c)[2:], nil); err != nil { // without the b/
return err
}
}
return nil
})
}
func (e *boltEng) ByID(id string) ([]byte, error) {
var out []byte
err := e.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket([]byte("d")).Get([]byte(id))
if v == nil {
return fmt.Errorf("not found")
}
out = append([]byte(nil), v...)
return nil
})
return out, err
}
func (e *boltEng) scan(tx *bolt.Tx, board string, f func(id string) error) error {
p := boardPrefix(board)[2:]
c := tx.Bucket([]byte("b")).Cursor()
for k, _ := c.Seek(p); k != nil && bytes.HasPrefix(k, p); k, _ = c.Next() {
if err := f(idFromIndexKey(k)); err != nil {
return err
}
}
return nil
}
func (e *boltEng) BoardCards(board string) ([][]byte, error) {
var out [][]byte
err := e.db.View(func(tx *bolt.Tx) error {
d := tx.Bucket([]byte("d"))
return e.scan(tx, board, func(id string) error {
out = append(out, append([]byte(nil), d.Get([]byte(id))...))
return nil
})
})
return out, err
}
func (e *boltEng) CountBoard(board string) (int, error) {
n := 0
err := e.db.View(func(tx *bolt.Tx) error { return e.scan(tx, board, func(string) error { n++; return nil }) })
return n, err
}
func (e *boltEng) Update(id string, change func([]byte) ([]byte, error)) error {
return e.db.Update(func(tx *bolt.Tx) error {
d := tx.Bucket([]byte("d"))
cur := d.Get([]byte(id))
if cur == nil {
return fmt.Errorf("not found")
}
next, err := change(append([]byte(nil), cur...))
if err != nil {
return err
}
return d.Put([]byte(id), next)
})
}
func (e *boltEng) Insert(c Card) error {
return e.db.Update(func(tx *bolt.Tx) error {
if err := tx.Bucket([]byte("d")).Put([]byte(c.ID), c.Raw); err != nil {
return err
}
return tx.Bucket([]byte("b")).Put(indexKey(c)[2:], nil)
})
}
func (e *boltEng) SizeBytes() int64 { return dirSize(e.dir) }
func (e *boltEng) Close() error { return e.db.Close() }
@@ -0,0 +1,101 @@
package main
import (
"encoding/json"
"fmt"
"path/filepath"
"github.com/tidwall/buntdb"
)
// BuntDB keeps everything in memory and writes an append-only file. It has indexes
// on JSON fields of its own, so no index keys are built by hand.
type buntEng struct {
db *buntdb.DB
dir string
}
func openBunt() maker {
return maker{name: "BuntDB (in memory with an append-only file)", open: func(dir string) (Engine, error) {
db, err := buntdb.Open(filepath.Join(dir, "cards.db"))
if err != nil {
return nil, err
}
return &buntEng{db: db, dir: dir}, nil
}}
}
func (e *buntEng) Name() string { return "BuntDB" }
func (e *buntEng) Load(cards []Card) error {
if err := e.db.Update(func(tx *buntdb.Tx) error {
for _, c := range cards {
if _, _, err := tx.Set("card:"+c.ID, string(c.Raw), nil); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
return e.db.CreateIndex("board", "card:*", buntdb.IndexJSON("boardId"), buntdb.IndexJSON("archived"))
}
func (e *buntEng) ByID(id string) ([]byte, error) {
var out []byte
err := e.db.View(func(tx *buntdb.Tx) error {
v, err := tx.Get("card:" + id)
out = []byte(v)
return err
})
return out, err
}
func (e *buntEng) pivot(board string) string {
b, _ := json.Marshal(board)
return fmt.Sprintf(`{"boardId":%s,"archived":false}`, b)
}
func (e *buntEng) BoardCards(board string) ([][]byte, error) {
var out [][]byte
err := e.db.View(func(tx *buntdb.Tx) error {
return tx.AscendEqual("board", e.pivot(board), func(_, v string) bool {
out = append(out, []byte(v))
return true
})
})
return out, err
}
func (e *buntEng) CountBoard(board string) (int, error) {
n := 0
err := e.db.View(func(tx *buntdb.Tx) error {
return tx.AscendEqual("board", e.pivot(board), func(_, _ string) bool { n++; return true })
})
return n, err
}
func (e *buntEng) Update(id string, change func([]byte) ([]byte, error)) error {
return e.db.Update(func(tx *buntdb.Tx) error {
v, err := tx.Get("card:" + id)
if err != nil {
return err
}
next, err := change([]byte(v))
if err != nil {
return err
}
_, _, err = tx.Set("card:"+id, string(next), nil)
return err
})
}
func (e *buntEng) Insert(c Card) error {
return e.db.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set("card:"+c.ID, string(c.Raw), nil)
return err
})
}
func (e *buntEng) SizeBytes() int64 { return dirSize(e.dir) }
func (e *buntEng) Close() error { return e.db.Close() }
@@ -0,0 +1,143 @@
package main
import (
"encoding/json"
c "github.com/ostafen/clover/v2"
d "github.com/ostafen/clover/v2/document"
q "github.com/ostafen/clover/v2/query"
)
// Clover is a document database with indexes and queries of its own, over Badger.
type cloverEng struct {
db *c.DB
dir string
}
func openClover() maker {
return maker{name: "Clover (document database over Badger)", open: func(dir string) (Engine, error) {
db, err := c.Open(dir)
if err != nil {
return nil, err
}
if err := db.CreateCollection("cards"); err != nil {
return nil, err
}
return &cloverEng{db: db, dir: dir}, nil
}}
}
func (e *cloverEng) Name() string { return "Clover" }
func toDoc(raw []byte) (*d.Document, error) {
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
// _id is Clover's own (a UUID); the card's id is kept in cid.
m["cid"] = m["_id"]
delete(m, "_id")
return d.NewDocumentOf(m), nil
}
// fromDoc is the card as JSON again, with its own _id back.
func fromDoc(doc *d.Document) ([]byte, error) {
m := map[string]any{}
for k, v := range doc.AsMap() {
m[k] = v
}
m["_id"] = m["cid"]
delete(m, "cid")
delete(m, "_id_clover")
return json.Marshal(m)
}
func (e *cloverEng) Load(cards []Card) error {
docs := make([]*d.Document, 0, len(cards))
for _, card := range cards {
doc, err := toDoc(card.Raw)
if err != nil {
return err
}
docs = append(docs, doc)
}
if err := e.db.Insert("cards", docs...); err != nil {
return err
}
for _, f := range []string{"cid", "boardId"} {
if err := e.db.CreateIndex("cards", f); err != nil {
return err
}
}
return nil
}
func (e *cloverEng) one(id string) (*d.Document, error) {
return e.db.FindFirst(q.NewQuery("cards").Where(q.Field("cid").Eq(id)))
}
func (e *cloverEng) ByID(id string) ([]byte, error) {
doc, err := e.one(id)
if err != nil || doc == nil {
return nil, err
}
return fromDoc(doc)
}
func (e *cloverEng) query(board string) *q.Query {
return q.NewQuery("cards").Where(q.Field("boardId").Eq(board).And(q.Field("archived").Eq(false)))
}
func (e *cloverEng) BoardCards(board string) ([][]byte, error) {
docs, err := e.db.FindAll(e.query(board))
if err != nil {
return nil, err
}
// Clover returns decoded documents. They are encoded again so the runner decodes
// them like every other engine's - which charges Clover for a step it does not need.
out := make([][]byte, 0, len(docs))
for _, doc := range docs {
b, err := fromDoc(doc)
if err != nil {
return nil, err
}
out = append(out, b)
}
return out, nil
}
func (e *cloverEng) CountBoard(board string) (int, error) { return e.db.Count(e.query(board)) }
func (e *cloverEng) Update(id string, change func([]byte) ([]byte, error)) error {
doc, err := e.one(id)
if err != nil {
return err
}
raw, err := fromDoc(doc)
if err != nil {
return err
}
next, err := change(raw)
if err != nil {
return err
}
var m map[string]any
if err := json.Unmarshal(next, &m); err != nil {
return err
}
m["cid"] = m["_id"]
delete(m, "_id")
return e.db.Update(q.NewQuery("cards").Where(q.Field("cid").Eq(id)), m)
}
func (e *cloverEng) Insert(card Card) error {
doc, err := toDoc(card.Raw)
if err != nil {
return err
}
_, err = e.db.InsertOne("cards", doc)
return err
}
func (e *cloverEng) SizeBytes() int64 { return dirSize(e.dir) }
func (e *cloverEng) Close() error { return e.db.Close() }
@@ -0,0 +1,17 @@
package main
// engines lists what is compared, the baseline first.
func engines() []maker {
return []maker{
openSQLite("SQLite as FerretDB uses it (JSON, expression indexes)", "normal", false),
openSQLite("SQLite, plain columns (not what FerretDB does)", "normal", true),
openSQLite("SQLite as FerretDB uses it, synchronous=FULL", "full", false),
openPebble(),
openBadger(),
openBolt("bbolt, no fsync", true),
openBolt("bbolt, fsync on every commit (its default)", false),
openBunt(),
openNuts(),
openClover(),
}
}
@@ -0,0 +1,35 @@
'use strict';
// Writes the speed test's deterministic data as JSON lines for the Go benchmark of
// embedded engines (bench.go): the cards, with the fields the queries read, and the
// parameters (the 3 biggest boards and 12 ordinary ones) the same way run.cjs picks
// them. node embedded/export.cjs <scale> <out-directory>
const fs = require('fs');
const path = require('path');
const data = require('../data.cjs');
const scale = Number(process.argv[2] || '0.05');
const out = path.resolve(process.argv[3] || '.');
fs.mkdirSync(out, { recursive: true });
const plan = data.plan({ scale });
const lines = [];
const perBoard = new Map();
let n = 0;
for (const batch of data.documents(plan, 'cards', 5000)) {
for (const doc of batch) {
lines.push(JSON.stringify(doc, (k, v) => (v instanceof Date ? { $date: v.getTime() } : v)));
perBoard.set(doc.boardId, (perBoard.get(doc.boardId) || 0) + 1);
n += 1;
}
}
fs.writeFileSync(path.join(out, 'cards.jsonl'), `${lines.join('\n')}\n`);
// Parameters: the 3 biggest boards, then 12 ordinary ones spread over the rest.
const boards = [...perBoard.entries()].sort((a, b) => b[1] - a[1]);
const biggest = boards.slice(0, 3).map(b => b[0]);
const rest = boards.slice(3);
const step = Math.max(1, Math.floor(rest.length / 12));
const ordinary = Array.from({ length: 12 }, (_, i) => rest[Math.min(rest.length - 1, i * step)][0]);
fs.writeFileSync(path.join(out, 'params.json'), JSON.stringify({ scale, cards: n, boards: [...biggest, ...ordinary], sizes: boards.slice(0, 3).map(b => b[1]) }));
console.log(`${n} cards, ${perBoard.size} boards; biggest boards have ${boards.slice(0, 3).map(b => b[1]).join(', ')} cards`);
@@ -0,0 +1,86 @@
module embeddedbench
go 1.26.0
require (
github.com/cockroachdb/pebble v1.1.5
github.com/dgraph-io/badger/v4 v4.9.6
github.com/nutsdb/nutsdb v1.1.0
github.com/ostafen/clover/v2 v2.0.0-alpha.3
github.com/tidwall/buntdb v1.3.2
go.etcd.io/bbolt v1.5.0
modernc.org/sqlite v1.61.0
)
require (
github.com/DataDog/zstd v1.4.5 // indirect
github.com/antlabs/stl v0.0.2 // indirect
github.com/antlabs/timer v0.1.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.1.0 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/orderedcode v0.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/ncruces/go-strftime v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/net v0.62.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.79.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
)
@@ -0,0 +1,379 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antlabs/stl v0.0.2 h1:sna1AXR5yIkNE9lWhCcKbheFJSVfCa3vugnGyakI79s=
github.com/antlabs/stl v0.0.2/go.mod h1:kKrO4xrn9cfS1mJVo+/BqePZjAYMXqD0amGF2Ouq7ac=
github.com/antlabs/timer v0.1.4 h1:MHdE00MDnNfhJCmqSOdLXs35uGNwfkMwfbynxrGmQ1c=
github.com/antlabs/timer v0.1.4/go.mod h1:mpw4zlD5KVjstEyUDp43DGLWsY076Mdo4bS78NTseRE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/brianvoe/gofakeit/v6 v6.17.0 h1:obbQTJeHfktJtiZzq0Q1bEpsNUs+yHrYlPVWt7BtmJ4=
github.com/brianvoe/gofakeit/v6 v6.17.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
github.com/dgraph-io/badger/v4 v4.9.6 h1:IQqMPVGLNCQr1b4Mu8lHkYm/xyqFRsyKaFEtyLi9CCQ=
github.com/dgraph-io/badger/v4 v4.9.6/go.mod h1:Xa9dAupjbwAacupWFCpa6YEn9E1PjBXkfZYr2I/8aWg=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us=
github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20=
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50 h1:PlNWJRxu5APSV0pWqfIsZ0qt9OOGHSWfZQ4zvx+WgWs=
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50/go.mod h1:yUo1AMMpsPIDFGuwDK8nLcf8+vvAd5l2O6JDZ6hpwrM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
github.com/nutsdb/nutsdb v1.1.0 h1:fNGFzBHGqF2mB5BF8Qk8W94c3/ZzwdCdKAH7azwx70Y=
github.com/nutsdb/nutsdb v1.1.0/go.mod h1:aKCtgSprZf2Mp1dIQD00Iya3DttoTErSSOnRx5ZtpAs=
github.com/ostafen/clover/v2 v2.0.0-alpha.3 h1:fXC7tVHQkUPFlxlj/kD98h0ngrTpIeJymaxVIqDzw3Q=
github.com/ostafen/clover/v2 v2.0.0-alpha.3/go.mod h1:5YCDt+wJDUNN1uSXE5csxSQBuJrNjidkOkJTXWuNhDY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235 h1:w0si+uee0iAaCJO9q86T6yrhdadgcsoNuh47LrUykzg=
github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235/go.mod h1:MR4+0R6A9NS5IABnIM3384FfOq8QFVnm7WDrBOhIaMU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys=
modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4=
modernc.org/ccgo/v4 v4.37.0/go.mod h1:/8627VhwWtAbqUXmPORxFQU5SgLrAx2jl9seLw9GBhY=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o=
modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.79.0 h1:9Dvvv++bAMSkSuBzuOFAatbTV5OIoq6Y6FohmD3e7q8=
modernc.org/libc v1.79.0/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.61.0 h1:sQi63oLQ9uquAUA4CzOisH35Q3pEszkPBuHM0tFL8CY=
modernc.org/sqlite v1.61.0/go.mod h1:f4gr5tbpx2umklaT/QVtbi/50hNdM+4ng+2zMo8KXJ0=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
@@ -0,0 +1,31 @@
package main
import "strings"
// The key layout a handler on a key-value store would have to build: the document
// under d/<id>, and the index of (boardId, archived) as keys b/<board>/<0|1>/<id>
// with no value. A scan of the prefix b/<board>/0/ is "the unarchived cards of a
// board" and counting its keys is the count.
func docKey(id string) []byte { return []byte("d/" + id) }
func boardPrefix(board string) []byte { return []byte("b/" + board + "/0/") }
func indexKey(c Card) []byte {
a := "0"
if c.Archived {
a = "1"
}
return []byte("b/" + c.Board + "/" + a + "/" + c.ID)
}
func idFromIndexKey(k []byte) string {
s := string(k)
return s[strings.LastIndexByte(s, '/')+1:]
}
// prefixEnd is the first key after every key with the prefix.
func prefixEnd(p []byte) []byte {
end := append([]byte(nil), p...)
end[len(end)-1]++
return end
}
@@ -0,0 +1,159 @@
// Embedded engines for FerretDB, compared with the SQLite it uses today.
//
// FerretDB is one Go binary, and its SQLite handler is the modernc.org/sqlite driver
// inside it. Which of the databases implemented in Go (awesome-go) could be linked in
// beside it, and is any of them faster for what WeKan asks? This is the engine
// level of that question: the same cards, the same questions, asked of each engine
// the way a handler written for it would ask. See README.md.
//
// go run . -data <dir with cards.jsonl and params.json> [-only sqlite,pebble] [-out results.json]
package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// Card is one WeKan card: what the questions read, and the whole document.
type Card struct {
ID string `json:"_id"`
Board string `json:"boardId"`
List string `json:"listId"`
Archived bool `json:"archived"`
Raw []byte `json:"-"`
}
// Engine is what a FerretDB handler needs from a storage engine for WeKan.
type Engine interface {
Name() string
// Load stores the cards and builds the indexes: by _id, and by (boardId, archived).
Load(cards []Card) error
ByID(id string) ([]byte, error)
// BoardCards returns the unarchived cards of a board.
BoardCards(board string) ([][]byte, error)
CountBoard(board string) (int, error)
// Update reads the card, changes it in Go and writes it back, which is what
// FerretDB's updateOne does.
Update(id string, change func([]byte) ([]byte, error)) error
Insert(c Card) error
SizeBytes() int64
Close() error
}
type params struct {
Scale float64 `json:"scale"`
Cards int `json:"cards"`
Boards []string `json:"boards"`
}
func main() {
dataDir := flag.String("data", "", "directory with cards.jsonl and params.json")
only := flag.String("only", "", "comma separated engine names to run")
outFile := flag.String("out", "", "write the results as JSON")
workDir := flag.String("work", "", "where the engines keep their files (default: a temporary directory)")
flag.Parse()
cards, p := readData(*dataDir)
byBoard := map[string][]Card{}
for _, c := range cards {
byBoard[c.Board] = append(byBoard[c.Board], c)
}
root := *workDir
if root == "" {
var err error
root, err = os.MkdirTemp("", "embeddedbench")
must(err)
}
defer os.RemoveAll(root)
var results []Result
for _, mk := range engines() {
name := mk.name
if *only != "" && !contains(strings.Split(*only, ","), name) {
continue
}
dir := filepath.Join(root, strings.NewReplacer(" ", "_", "/", "_", "(", "", ")", "").Replace(name))
must(os.MkdirAll(dir, 0o755))
fmt.Fprintf(os.Stderr, "== %s\n", name)
res, err := run(mk.open, name, dir, cards, byBoard, p)
if err != nil {
fmt.Fprintf(os.Stderr, " FAILED: %v\n", err)
results = append(results, Result{Name: name, Error: err.Error()})
continue
}
results = append(results, *res)
}
report(results, p)
if *outFile != "" {
b, _ := json.MarshalIndent(results, "", " ")
must(os.WriteFile(*outFile, b, 0o644))
}
}
func readData(dir string) ([]Card, params) {
var p params
b, err := os.ReadFile(filepath.Join(dir, "params.json"))
must(err)
must(json.Unmarshal(b, &p))
f, err := os.Open(filepath.Join(dir, "cards.jsonl"))
must(err)
defer f.Close()
var cards []Card
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1<<20), 1<<24)
for sc.Scan() {
line := bytes.TrimSpace(sc.Bytes())
if len(line) == 0 {
continue
}
var c Card
must(json.Unmarshal(line, &c))
c.Raw = append([]byte(nil), line...)
cards = append(cards, c)
}
must(sc.Err())
return cards, p
}
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func contains(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
func median(d []time.Duration) time.Duration {
sort.Slice(d, func(i, j int) bool { return d[i] < d[j] })
return d[len(d)/2]
}
// modify is the change an updateOne makes: decode, set, encode.
func modify(raw []byte, key string, value any) ([]byte, error) {
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
m[key] = value
return json.Marshal(m)
}
@@ -0,0 +1,135 @@
package main
import (
"bytes"
"errors"
"fmt"
"github.com/nutsdb/nutsdb"
)
type nutsEng struct {
db *nutsdb.DB
dir string
}
func openNuts() maker {
return maker{name: "Nutsdb", open: func(dir string) (Engine, error) {
db, err := nutsdb.Open(nutsdb.DefaultOptions, nutsdb.WithDir(dir), nutsdb.WithSyncEnable(false))
if err != nil {
return nil, err
}
for _, b := range []string{"d", "b"} {
if err := db.Update(func(tx *nutsdb.Tx) error { return tx.NewBucket(nutsdb.DataStructureBTree, b) }); err != nil {
return nil, err
}
}
return &nutsEng{db: db, dir: dir}, nil
}}
}
func (e *nutsEng) Name() string { return "Nutsdb" }
func (e *nutsEng) Load(cards []Card) error {
for start := 0; start < len(cards); start += 2000 {
end := min(start+2000, len(cards))
if err := e.db.Update(func(tx *nutsdb.Tx) error {
for _, c := range cards[start:end] {
if err := tx.Put("d", []byte(c.ID), c.Raw, nutsdb.Persistent); err != nil {
return err
}
if err := tx.Put("b", indexKey(c)[2:], []byte{1}, nutsdb.Persistent); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
}
return nil
}
func (e *nutsEng) ByID(id string) ([]byte, error) {
var out []byte
err := e.db.View(func(tx *nutsdb.Tx) error {
v, err := tx.Get("d", []byte(id))
out = append([]byte(nil), v...)
return err
})
return out, err
}
func (e *nutsEng) ids(tx *nutsdb.Tx, board string) ([]string, error) {
p := boardPrefix(board)[2:]
keys, _, err := tx.PrefixScanEntries("b", p, "", 0, 1<<30, true, false)
if err != nil {
if errors.Is(err, nutsdb.ErrPrefixScan) || errors.Is(err, nutsdb.ErrNotFoundKey) {
return nil, nil // a prefix with nothing under it is an error here, not an empty list
}
return nil, err
}
var ids []string
for _, k := range keys {
if !bytes.HasPrefix(k, p) {
continue
}
ids = append(ids, idFromIndexKey(k))
}
return ids, nil
}
func (e *nutsEng) BoardCards(board string) ([][]byte, error) {
var out [][]byte
err := e.db.View(func(tx *nutsdb.Tx) error {
ids, err := e.ids(tx, board)
if err != nil {
return err
}
for _, id := range ids {
v, err := tx.Get("d", []byte(id))
if err != nil {
return err
}
out = append(out, append([]byte(nil), v...))
}
return nil
})
return out, err
}
func (e *nutsEng) CountBoard(board string) (int, error) {
n := 0
err := e.db.View(func(tx *nutsdb.Tx) error {
ids, err := e.ids(tx, board)
n = len(ids)
return err
})
return n, err
}
func (e *nutsEng) Update(id string, change func([]byte) ([]byte, error)) error {
return e.db.Update(func(tx *nutsdb.Tx) error {
v, err := tx.Get("d", []byte(id))
if err != nil {
return fmt.Errorf("get %s: %w", id, err)
}
next, err := change(append([]byte(nil), v...))
if err != nil {
return err
}
return tx.Put("d", []byte(id), next, nutsdb.Persistent)
})
}
func (e *nutsEng) Insert(c Card) error {
return e.db.Update(func(tx *nutsdb.Tx) error {
if err := tx.Put("d", []byte(c.ID), c.Raw, nutsdb.Persistent); err != nil {
return err
}
return tx.Put("b", indexKey(c)[2:], []byte{1}, nutsdb.Persistent)
})
}
func (e *nutsEng) SizeBytes() int64 { return dirSize(e.dir) }
func (e *nutsEng) Close() error { return e.db.Close() }
@@ -0,0 +1,110 @@
package main
import (
"github.com/cockroachdb/pebble"
)
type pebbleEng struct {
db *pebble.DB
dir string
}
func openPebble() maker {
return maker{name: "Pebble (CockroachDB's LSM)", open: func(dir string) (Engine, error) {
db, err := pebble.Open(dir, &pebble.Options{})
if err != nil {
return nil, err
}
return &pebbleEng{db: db, dir: dir}, nil
}}
}
func (e *pebbleEng) Name() string { return "Pebble" }
func (e *pebbleEng) Load(cards []Card) error {
b := e.db.NewBatch()
for i, c := range cards {
if err := b.Set(docKey(c.ID), c.Raw, nil); err != nil {
return err
}
if err := b.Set(indexKey(c), nil, nil); err != nil {
return err
}
if i%5000 == 4999 {
if err := b.Commit(pebble.NoSync); err != nil {
return err
}
b = e.db.NewBatch()
}
}
if err := b.Commit(pebble.NoSync); err != nil {
return err
}
return e.db.Flush()
}
func (e *pebbleEng) ByID(id string) ([]byte, error) {
v, closer, err := e.db.Get(docKey(id))
if err != nil {
return nil, err
}
defer closer.Close()
return append([]byte(nil), v...), nil
}
func (e *pebbleEng) ids(board string) ([]string, error) {
p := boardPrefix(board)
it, err := e.db.NewIter(&pebble.IterOptions{LowerBound: p, UpperBound: prefixEnd(p)})
if err != nil {
return nil, err
}
defer it.Close()
var ids []string
for ok := it.First(); ok; ok = it.Next() {
ids = append(ids, idFromIndexKey(it.Key()))
}
return ids, it.Error()
}
func (e *pebbleEng) BoardCards(board string) ([][]byte, error) {
ids, err := e.ids(board)
if err != nil {
return nil, err
}
out := make([][]byte, 0, len(ids))
for _, id := range ids {
d, err := e.ByID(id)
if err != nil {
return nil, err
}
out = append(out, d)
}
return out, nil
}
func (e *pebbleEng) CountBoard(board string) (int, error) {
ids, err := e.ids(board)
return len(ids), err
}
func (e *pebbleEng) Update(id string, change func([]byte) ([]byte, error)) error {
d, err := e.ByID(id)
if err != nil {
return err
}
next, err := change(d)
if err != nil {
return err
}
return e.db.Set(docKey(id), next, pebble.NoSync)
}
func (e *pebbleEng) Insert(c Card) error {
b := e.db.NewBatch()
_ = b.Set(docKey(c.ID), c.Raw, nil)
_ = b.Set(indexKey(c), nil, nil)
return b.Commit(pebble.NoSync)
}
func (e *pebbleEng) SizeBytes() int64 { return dirSize(e.dir) }
func (e *pebbleEng) Close() error { return e.db.Close() }
@@ -0,0 +1,238 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
)
type maker struct {
name string
open func(dir string) (Engine, error)
}
// Result is one engine's numbers. Times are milliseconds for one call.
type Result struct {
Name string `json:"name"`
Error string `json:"error,omitempty"`
LoadMs float64 `json:"loadMs"`
SizeMB float64 `json:"sizeMB"`
Queries map[string]float64 `json:"queries"`
Slowest map[string]float64 `json:"slowest"`
TotalMs float64 `json:"totalMs"`
TotalSlow float64 `json:"totalSlowestMs"`
}
var queryOrder = []string{"byId", "boardCards", "countBoard", "update", "move", "insert"}
// sample times f: it is repeated until about 15 ms have passed (at most 400 times)
// and the mean of one call is returned, so a microsecond call is not lost in the
// clock. It is taken three times and the median returned.
func sample(f func(i int) error) (time.Duration, error) {
var out []time.Duration
for pass := 0; pass < 3; pass++ {
start := time.Now()
n := 0
for n < 400 && (n == 0 || time.Since(start) < 15*time.Millisecond) {
if err := f(n); err != nil {
return 0, err
}
n++
}
out = append(out, time.Since(start)/time.Duration(n))
}
return median(out), nil
}
func run(open func(string) (Engine, error), name, dir string, cards []Card, byBoard map[string][]Card, p params) (*Result, error) {
e, err := open(dir)
if err != nil {
return nil, err
}
defer e.Close()
res := &Result{Name: name, Queries: map[string]float64{}, Slowest: map[string]float64{}}
t0 := time.Now()
if err = e.Load(cards); err != nil {
return nil, fmt.Errorf("load: %w", err)
}
res.LoadMs = float64(time.Since(t0).Microseconds()) / 1000
res.SizeMB = float64(e.SizeBytes()) / (1 << 20)
// Correctness first: a fast wrong answer is no answer.
for _, b := range p.Boards {
want := 0
for _, c := range byBoard[b] {
if !c.Archived {
want++
}
}
docs, err := e.BoardCards(b)
if err != nil {
return nil, err
}
n, err := e.CountBoard(b)
if err != nil {
return nil, err
}
if len(docs) != want || n != want {
return nil, fmt.Errorf("board %s: %d cards and a count of %d, want %d", b, len(docs), n, want)
}
first := byBoard[b][0]
raw, err := e.ByID(first.ID)
if err != nil {
return nil, err
}
var back Card
if err := json.Unmarshal(raw, &back); err != nil || back.ID != first.ID || back.Board != first.Board {
return nil, fmt.Errorf("byId %s returned %q", first.ID, string(raw))
}
}
// Reads: each is asked with every parameter board, and the cell is the average of
// the per-parameter medians, like the speed test's. "Slowest" is the worst board.
reads := map[string]func(b string, i int) error{
"byId": func(b string, i int) error {
cs := byBoard[b]
_, err := e.ByID(cs[i%len(cs)].ID)
return err
},
"boardCards": func(b string, i int) error {
docs, err := e.BoardCards(b)
if err != nil {
return err
}
for _, d := range docs { // decoded, as FerretDB must
var m map[string]any
if err := json.Unmarshal(d, &m); err != nil {
return err
}
}
return nil
},
"countBoard": func(b string, i int) error { _, err := e.CountBoard(b); return err },
// The same fetch without decoding: the part that differs between engines.
"boardRaw": func(b string, i int) error { _, err := e.BoardCards(b); return err },
}
for _, q := range []string{"byId", "boardCards", "boardRaw", "countBoard"} {
var sum, worst float64
for _, b := range p.Boards {
f := reads[q]
d, err := sample(func(i int) error { return f(b, i) })
if err != nil {
return nil, fmt.Errorf("%s: %w", q, err)
}
ms := float64(d.Nanoseconds()) / 1e6
sum += ms
if ms > worst {
worst = ms
}
}
res.Queries[q] = sum / float64(len(p.Boards))
res.Slowest[q] = worst
}
// Writes: 200 different cards each, so nothing is a no-op; the mean of a call.
ids := make([]string, 0, 200)
for i := 0; i < len(cards) && len(ids) < 200; i += len(cards) / 200 {
ids = append(ids, cards[i].ID)
}
writes := []struct {
name string
f func(i int) error
}{
{"update", func(i int) error {
return e.Update(ids[i%len(ids)], func(raw []byte) ([]byte, error) { return modify(raw, "title", fmt.Sprintf("Edited %d", i)) })
}},
{"move", func(i int) error {
return e.Update(ids[(i+1)%len(ids)], func(raw []byte) ([]byte, error) {
m, err := modify(raw, "sort", 100000+i)
if err != nil {
return nil, err
}
return modify(m, "listId", "moved")
})
}},
{"insert", func(i int) error {
doc := fmt.Sprintf(`{"_id":"new%d","boardId":"%s","listId":"l","archived":false,"title":"New","sort":%d}`, i, p.Boards[0], i)
return e.Insert(Card{ID: fmt.Sprintf("new%d", i), Board: p.Boards[0], Raw: []byte(doc)})
}},
}
for _, w := range writes {
start := time.Now()
for i := 0; i < len(ids); i++ {
if err := w.f(i); err != nil {
return nil, fmt.Errorf("%s: %w", w.name, err)
}
}
ms := float64(time.Since(start).Nanoseconds()) / 1e6 / float64(len(ids))
res.Queries[w.name] = ms
res.Slowest[w.name] = ms
}
// The writes must be there.
raw, err := e.ByID(ids[0])
if err != nil {
return nil, err
}
if !strings.Contains(string(raw), `"moved"`) {
return nil, fmt.Errorf("the move was not stored: %s", raw)
}
for _, q := range queryOrder {
res.TotalMs += res.Queries[q]
res.TotalSlow += res.Slowest[q]
}
return res, nil
}
func report(results []Result, p params) {
ok := results[:0:0]
for _, r := range results {
if r.Error == "" {
ok = append(ok, r)
}
}
// Fastest first.
for i := range ok {
for j := i + 1; j < len(ok); j++ {
if ok[j].TotalMs < ok[i].TotalMs {
ok[i], ok[j] = ok[j], ok[i]
}
}
}
fmt.Printf("\nScale %v: %d cards, %d parameter boards. Milliseconds for one call; total = sum of the six.\n\n", p.Scale, p.Cards, len(p.Boards))
fmt.Println("| # | Engine | Total | By id | Board cards | (raw, not decoded) | Count | Edit | Move | Add | Load (s) | Size (MB) |")
fmt.Println("| ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |")
for i, r := range ok {
size := fmt.Sprintf("%.0f", r.SizeMB)
if r.SizeMB < 0 {
size = "n/a"
}
fmt.Printf("| %d | %s | **%s** | %s | %s | %s | %s | %s | %s | %s | %.1f | %s |\n", i+1, r.Name, ms(r.TotalMs),
ms(r.Queries["byId"]), ms(r.Queries["boardCards"]), ms(r.Queries["boardRaw"]), ms(r.Queries["countBoard"]),
ms(r.Queries["update"]), ms(r.Queries["move"]), ms(r.Queries["insert"]), r.LoadMs/1000, size)
}
for _, r := range results {
if r.Error != "" {
fmt.Printf("\n%s: %s\n", r.Name, r.Error)
}
}
_ = os.Stdout.Sync()
}
func ms(v float64) string {
switch {
case v < 0.01:
return fmt.Sprintf("%.4f", v)
case v < 10:
return fmt.Sprintf("%.3f", v)
case v < 100:
return fmt.Sprintf("%.1f", v)
default:
return fmt.Sprintf("%.0f", v)
}
}
@@ -0,0 +1,185 @@
package main
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"syscall"
_ "modernc.org/sqlite"
)
// SQLite as FerretDB uses it: modernc.org/sqlite, WAL, synchronous NORMAL, a 64 MiB
// cache and 256 MiB of mmap (FerretDB's defaults, metadata/pool/uri.go). The
// document is JSON text and the indexes are on json_extract expressions, which is
// how FerretDB indexes a field. "columns" keeps the same document but indexes real
// columns, which FerretDB does not do; it shows what SQLite itself could do.
type sqliteEng struct {
name string
db *sql.DB
dir string
columns bool
}
func openSQLite(name, sync string, columns bool) maker {
return maker{name: name, open: func(dir string) (Engine, error) {
path := filepath.Join(dir, "cards.sqlite")
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(wal)&_pragma=synchronous(%s)&_pragma=cache_size(-65536)&_pragma=mmap_size(268435456)&_pragma=temp_store(memory)&_pragma=busy_timeout(30000)&_txlock=immediate", path, sync)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
return &sqliteEng{name: name, db: db, dir: dir, columns: columns}, nil
}}
}
func (e *sqliteEng) Name() string { return e.name }
func (e *sqliteEng) Load(cards []Card) error {
create := `CREATE TABLE cards (doc TEXT NOT NULL)`
insert := `INSERT INTO cards (doc) VALUES (?)`
if e.columns {
create = `CREATE TABLE cards (id TEXT PRIMARY KEY, board TEXT, archived INTEGER, doc TEXT NOT NULL)`
insert = `INSERT INTO cards (id, board, archived, doc) VALUES (?, ?, ?, ?)`
}
if _, err := e.db.Exec(create); err != nil {
return err
}
tx, err := e.db.Begin()
if err != nil {
return err
}
st, err := tx.Prepare(insert)
if err != nil {
return err
}
for _, c := range cards {
if e.columns {
_, err = st.Exec(c.ID, c.Board, boolInt(c.Archived), string(c.Raw))
} else {
_, err = st.Exec(string(c.Raw))
}
if err != nil {
return err
}
}
if err = tx.Commit(); err != nil {
return err
}
idx := []string{
`CREATE UNIQUE INDEX cards_id ON cards (json_extract(doc, '$._id'))`,
`CREATE INDEX cards_board ON cards (json_extract(doc, '$.boardId'), json_extract(doc, '$.archived'))`,
}
if e.columns {
idx = []string{`CREATE INDEX cards_board ON cards (board, archived)`}
}
for _, q := range idx {
if _, err = e.db.Exec(q); err != nil {
return err
}
}
_, err = e.db.Exec(`ANALYZE`)
return err
}
func (e *sqliteEng) where() (id, board string) {
if e.columns {
return `id = ?`, `board = ? AND archived = 0`
}
return `json_extract(doc, '$._id') = ?`, `json_extract(doc, '$.boardId') = ? AND json_extract(doc, '$.archived') = 0`
}
func (e *sqliteEng) ByID(id string) ([]byte, error) {
w, _ := e.where()
var doc string
err := e.db.QueryRow(`SELECT doc FROM cards WHERE `+w, id).Scan(&doc)
return []byte(doc), err
}
func (e *sqliteEng) BoardCards(board string) ([][]byte, error) {
_, w := e.where()
rows, err := e.db.Query(`SELECT doc FROM cards WHERE `+w, board)
if err != nil {
return nil, err
}
defer rows.Close()
var out [][]byte
for rows.Next() {
var doc string
if err := rows.Scan(&doc); err != nil {
return nil, err
}
out = append(out, []byte(doc))
}
return out, rows.Err()
}
func (e *sqliteEng) CountBoard(board string) (int, error) {
_, w := e.where()
var n int
err := e.db.QueryRow(`SELECT COUNT(*) FROM cards WHERE `+w, board).Scan(&n)
return n, err
}
func (e *sqliteEng) Update(id string, change func([]byte) ([]byte, error)) error {
w, _ := e.where()
tx, err := e.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var doc string
if err = tx.QueryRow(`SELECT doc FROM cards WHERE `+w, id).Scan(&doc); err != nil {
return err
}
next, err := change([]byte(doc))
if err != nil {
return err
}
if _, err = tx.Exec(`UPDATE cards SET doc = ? WHERE `+w, string(next), id); err != nil {
return err
}
return tx.Commit()
}
func (e *sqliteEng) Insert(c Card) error {
if e.columns {
_, err := e.db.Exec(`INSERT INTO cards (id, board, archived, doc) VALUES (?, ?, ?, ?)`, c.ID, c.Board, boolInt(c.Archived), string(c.Raw))
return err
}
_, err := e.db.Exec(`INSERT INTO cards (doc) VALUES (?)`, string(c.Raw))
return err
}
func (e *sqliteEng) SizeBytes() int64 {
_, _ = e.db.Exec(`PRAGMA wal_checkpoint(TRUNCATE)`)
return dirSize(e.dir)
}
func (e *sqliteEng) Close() error { return e.db.Close() }
func boolInt(b bool) int {
if b {
return 1
}
return 0
}
// dirSize is what the files really occupy: the blocks, not the apparent size, which
// counts the sparse files Badger preallocates.
func dirSize(dir string) int64 {
var n int64
_ = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() {
if st, ok := info.Sys().(*syscall.Stat_t); ok {
n += st.Blocks * 512
} else {
n += info.Size()
}
}
return nil
})
return n
}
+59 -12
View File
@@ -99,11 +99,42 @@ function compare(runs, reference) {
function machine() {
const cpus = os.cpus();
return `${cpus[0] ? cpus[0].model.trim() : os.arch()}, ${cpus.length} threads, ` +
`${Math.round(os.totalmem() / 2 ** 30)} GiB RAM, Linux ${os.release()}`;
`${Math.round(os.totalmem() / 2 ** 30)} GiB RAM, ${os.type()} ${os.release()}`;
}
// What the scale means, in words: 2 is twice a production WeKan, 0.05 a twentieth.
function sizeWords(scale) {
if (scale === 1) return 'as big as a production WeKan';
if (scale === 2) return 'twice a production WeKan';
if (scale > 1) return `${scale} times a production WeKan`;
const n = Math.round(1 / scale);
return Math.abs(1 / scale - n) < 0.01 ? `1/${n} of a production WeKan` : `${Math.round(scale * 100)}% of a production WeKan`;
}
// How many times slower than the fastest, as a person reads it: "1.0x", "4.9x",
// "27x", "1,300x".
function times(v, fastest) {
if (!Number.isFinite(v) || !fastest) return '-';
const r = v / fastest;
if (r < 10) return `${r.toFixed(1)}x`;
return `${Math.round(r).toLocaleString('en-US')}x`;
}
function render(runs, sameness, reference, dir) {
const sorted = [...runs].sort((a, b) => total(a) - total(b) || a.label.localeCompare(b.label));
// Fastest first, by the sum of a row. Every table below is ordered by ITS OWN
// numbers, so each reads from fastest to slowest.
const byTotal = (totalOf) => [...runs].sort((a, b) => totalOf(a) - totalOf(b) || a.label.localeCompare(b.label));
const slowestTotal = (run) => QUERIES.reduce((sum, q) => {
const r = run.queries[q.id];
return r && !r.error && r.slowest !== undefined ? sum + r.slowest : Infinity;
}, 0);
const loadTotal = (run) => {
const l = run.load || {};
const parts = ['users', 'boards', 'lists', 'swimlanes', 'cards'].map(c => l[c] && l[c].ms);
return parts.some(v => v === undefined) || run.indexTotalMs === undefined
? Infinity : parts.reduce((a, b) => a + b, 0) + run.indexTotalMs;
};
const sorted = byTotal(total);
const first = runs.map(r => r.started).sort()[0] || '';
const sizes = (runs[0] && runs[0].sizes) || {};
const lines = [];
@@ -111,16 +142,17 @@ function render(runs, sameness, reference, dir) {
lines.push(`Run ${first.slice(0, 10)} on ${machine()}, by`);
lines.push('[speedtest.sh](speedtest.sh). How it works, and what was improved because of it:');
lines.push('[the README](.).', '');
lines.push(`The data is twice a production WeKan: ${['users', 'boards', 'lists', 'swimlanes', 'cards']
lines.push(`The data is ${sizeWords(runs[0] && runs[0].scale)}: ${['users', 'boards', 'lists', 'swimlanes', 'cards']
.map(c => `${(sizes[c] || 0).toLocaleString('en-US')} ${c}`).join(', ')}` +
` (production: ${PRODUCTION.cards.toLocaleString('en-US')} cards). Every database got identical`);
lines.push('documents and WeKan\'s own indexes, and was asked identical questions.', '');
const table = (field, totalOf) => {
const order = byTotal(totalOf);
const head = ['Database', 'Total', ...QUERIES.map(q => q.title)];
lines.push(`| ${head.join(' | ')} |`);
lines.push(`| ${head.map((_, i) => (i === 0 ? '---' : '---:')).join(' | ')} |`);
for (const run of sorted) {
for (const run of order) {
const t = totalOf(run);
const cells = QUERIES.map((q) => {
const r = run.queries[q.id];
@@ -132,6 +164,22 @@ function render(runs, sameness, reference, dir) {
}
lines.push('');
};
lines.push('## At a glance', '');
lines.push('One number per database: the time of one call of each of the common WeKan queries below,');
lines.push('added together. **Times slower** is that sum divided by the fastest database\'s. Ordered');
lines.push('from fastest to slowest. The second pair of columns is the same for the slowest parameter -');
lines.push('the biggest board or the busiest user, who waits longest.', '');
lines.push('| # | Database | Total (ms) | Times slower | Slowest user (ms) | Times slower |');
lines.push('| ---: | --- | ---: | ---: | ---: | ---: |');
const fastestAvg = Math.min(...runs.map(total));
const fastestSlow = Math.min(...runs.map(slowestTotal));
sorted.forEach((run, i) => {
const t = total(run);
const w = slowestTotal(run);
lines.push(`| ${i + 1} | ${mdCell(run.engine)} | ${Number.isFinite(t) ? ms(t) : 'incomplete'} | ${times(t, fastestAvg)} | ` +
`${Number.isFinite(w) ? ms(w) : 'incomplete'} | ${times(w, fastestSlow)} |`);
});
lines.push('');
lines.push('## Speed', '');
lines.push('Milliseconds for one call - smaller is faster. Each query is asked with 15 parameters (the');
lines.push('3 biggest boards or busiest users and 12 ordinary ones, see below); a cell is the average of');
@@ -140,17 +188,16 @@ function render(runs, sameness, reference, dir) {
table('average', total);
lines.push('### The slowest parameter', '');
lines.push('The same, for the slowest of the 15 parameters - nearly always the biggest board or the');
lines.push('busiest user. This is how long the person who waits longest waits. Same row order.', '');
table('slowest', (run) => QUERIES.reduce((sum, q) => {
const r = run.queries[q.id];
return r && !r.error && r.slowest !== undefined ? sum + r.slowest : Infinity;
}, 0));
lines.push('busiest user. This is how long the person who waits longest waits. Ordered by its own');
lines.push('total, so the order can differ from the table above.', '');
table('slowest', slowestTotal);
lines.push('## Loading the data', '');
lines.push('Inserting the documents in batches of 1,000, then creating every index WeKan creates.', '');
lines.push('Inserting the documents in batches of 1,000, then creating every index WeKan creates.');
lines.push('Ordered by the whole time, fastest first.', '');
lines.push('| Database | users | boards | lists | swimlanes | cards | Indexes | Index errors |');
lines.push('| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |');
for (const run of sorted) {
for (const run of byTotal(loadTotal)) {
const l = run.load || {};
const ixErrors = Object.entries(run.indexes || {}).filter(([, v]) => v.error).map(([k]) => mdCell(k));
lines.push(`| ${mdCell(run.engine)} | ${['users', 'boards', 'lists', 'swimlanes', 'cards'].map(c => seconds(l[c] && l[c].ms)).join(' | ')} | ` +
@@ -182,7 +229,7 @@ function render(runs, sameness, reference, dir) {
lines.push('');
const calls = runs.length ? Object.values(runs[0].queries).map(r => r.calls).filter(Boolean) : [];
lines.push(`Each query is first run once per parameter to warm up (those answers are the ones compared),`);
lines.push(`then repeated for up to 5 passes or 10 seconds; ${calls.length ? `${Math.min(...calls)}-${Math.max(...calls)}` : 'the'} calls per query were timed.`);
lines.push(`then repeated for up to 5 passes or the time budget (WEKAN_SPEEDTEST_BUDGET_MS); ${calls.length ? `${Math.min(...calls)}-${Math.max(...calls)}` : 'the'} calls per query were timed.`);
lines.push('A query too slow to be repeated in that time keeps its warm-up time.');
lines.push(`Raw results: \`${path.relative(path.resolve(__dirname, '../../../../..'), dir) || dir}/speedtest-<database>.json\`.`, '');
return `${lines.join('\n')}\n`;
@@ -0,0 +1 @@
{"uuid":"fb14521d-8728-4fdb-befd-132ef195c35c","telemetry":false}
@@ -0,0 +1,62 @@
# Version history in the database: Dolt and TerminusDB, and WeKan's undo and redo
WeKan's undo and redo (Ctrl+Z, Ctrl+Y, History → Restore, see
[History](../../../../Features/General/History/History.md) and
[Undo](../../../../Features/General/Undo/Undo.md)) is application code: one
`changeHistory` row per change, with who, when and which field, and a restore that
reapplies the old values oldest to newest. Some databases version their data
themselves. This asks what that would give WeKan, by trying WeKan-shaped changes
(edit a card's title, move it, archive it, delete another one) on two of them.
```sh
# Dolt, under FerretDB's MySQL handler - WeKan's writes arrive over the MongoDB protocol
docker run -d --name dolttry -p 3307:3306 -e DOLT_ROOT_PASSWORD=rootpw -e DOLT_ROOT_HOST=% \
-e DOLT_DATABASE=ferretdb dolthub/dolt-sql-server:latest
mysql ... -e 'SET GLOBAL dolt_transaction_commit = 1' # a Dolt commit per transaction
ferretdb --handler=mysql --mysql-url='mysql://root:rootpw@127.0.0.1:3307/ferretdb' --listen-addr=127.0.0.1:27022
node docs/Databases/FerretDB/1/VersionHistory/dolt-history.cjs
# TerminusDB - REST only
docker run -d --name tdbtry -p 6363:6363 terminusdb/terminusdb-server:latest
node docs/Databases/FerretDB/1/VersionHistory/terminusdb-history.cjs
```
Run on 2026-11-03: Dolt (sql-server image, MySQL 8.0.31 protocol) with FerretDB
v1.105.0, and TerminusDB 12.0.9. Both are open source (Apache-2.0).
## What was found
| WeKan needs | Dolt under FerretDB | TerminusDB |
| --- | --- | --- |
| A record of every change | **Yes, with no change to WeKan.** With `dolt_transaction_commit` on, every FerretDB write is one Dolt commit (four writes, four commits). | Yes, one commit per write. |
| Who made it | **No.** The committer is the SQL user (`root`) for every WeKan user, and the message is always `Transaction commit`. FerretDB would have to set them. | **Yes.** Each commit has the author and message the caller gives: `alice \| edit title`. |
| What changed | Whole documents: an edit is a removed row and an added row in `dolt_diff_<table>`, because FerretDB's tables have no primary key and a card is one JSON cell. Fields are found by comparing the two. | **Field by field**: `{"title": {"@before": "Write docs v2", "@after": "Write docs", "@op": "SwapValue"}}`. |
| Undo the last change | **Yes.** `CALL DOLT_REVERT('HEAD')` brought a deleted card back, read through FerretDB over the MongoDB protocol; reverting that revert is the redo. Both are commits of their own, so history is never rewritten. | **Yes**: diff the last two commits and apply it backwards (`/api/diff`, `/api/patch`). |
| Restore one older change, keep the later ones | **Another document: yes. The same document: no.** Reverting an old title edit of card c1 after c1 was moved and archived failed with `the table(s) ... are in conflict` (the whole document is one cell). The failed revert left nothing behind, and FerretDB writes kept working. | **Yes, by field.** The title was restored while the move and the archive stayed: `title: "Write docs", listId: "l2", archived: true`. A patch whose before-value no longer matches is the conflict. |
| The board as it was | **Yes**, in SQL: `SELECT ... AS OF '<commit>'`. FerretDB does not offer it over the MongoDB protocol. | **Yes**: `GET /api/document/.../local/commit/<id>`. |
| Undo "my last change" or "this board's last change" | **No.** `HEAD` is the last commit of anyone on anything. A scope means choosing commits by their diff. | The log is per database; a scope is also chosen by the diff. |
| Cost per write | **+0.4 ms**: `updateOne` took 2.34 ms with commits off and 2.77 ms with them on (200 calls each), +18%. 223 commits for 200 updates. | About 25 ms per `PUT` of a card, an HTTP call and a commit each. |
| Can WeKan run on it | **Yes**: it is a MySQL-protocol database that FerretDB's MySQL handler already runs (it is in the [speed test](../SpeedTest/)). | **No.** It has neither the MongoDB protocol nor SQL; WeKan would need a new data layer. |
## What it means for WeKan
- **WeKan's own History is still the right undo.** It knows the user, the field,
and the card, list or swimlane a change belongs to, and it restores one field
while later changes stay. Dolt cannot do that for a document edited again
afterwards, and it does not know who made a change; TerminusDB can, but WeKan
cannot run on it.
- **Dolt would add something WeKan does not have: the whole database at any
commit.** Restoring a board, or all of them, as they were an hour ago is
`AS OF` or `DOLT_RESET`, with no code in WeKan. It is a backup and audit layer
under the undo, not a replacement for it. Its costs are real: a commit per
write of everything WeKan writes (sessions, activities and search sessions
included, which are not the user's changes), so the history is mostly noise.
And Dolt is slow for WeKan's reads: in the [speed test](../SpeedTest/Results.md)
every query over cards takes about 2.8 s on 15,377 cards (MySQL: 1-8 ms), 20,470
ms in total against MySQL's 103, because it scans the table instead of using the
indexes. A production-size WeKan on it would not be usable as it is.
- **What FerretDB would need for Dolt to answer "who"**: set the commit author and
message from the MongoDB command's `comment`, or the connection's user. It does
neither today.
- **Not tried**: Dolt branches and merges for a draft board, and TerminusDB's
history of a single document, which would answer "this card's history" directly.
@@ -0,0 +1,134 @@
'use strict';
// What a database with version history gives WeKan's undo and redo: Dolt (git for
// data, MySQL protocol) under FerretDB's MySQL handler. WeKan writes through the
// MongoDB protocol as always; every FerretDB write is a Dolt transaction, and with
// dolt_transaction_commit on every transaction is a Dolt commit.
//
// node docs/Databases/FerretDB/1/VersionHistory/dolt-history.cjs \
// --uri 'mongodb://127.0.0.1:27022/?directConnection=true' --sql-port 3307 --sql-password rootpw
//
// It prints what it finds; README.md next to it explains the findings. The SQL side
// is asked through the mysql client of the mysql:9 image, so nothing is installed.
const { execFileSync } = require('child_process');
const { MongoClient } = require('mongodb');
function arg(name, fallback) {
const i = process.argv.indexOf(`--${name}`);
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
}
const URI = arg('uri', 'mongodb://127.0.0.1:27022/?directConnection=true');
const PORT = arg('sql-port', '3307');
const PASSWORD = arg('sql-password', 'rootpw');
const DB = 'wekanhistory';
const TIMING_ONLY = process.argv.includes('--timing');
function sql(query, { db = DB, allowError = false } = {}) {
try {
return execFileSync('docker', ['run', '--rm', '--network', 'host', 'mysql:9', 'mysql',
'-h', '127.0.0.1', '-P', PORT, '-u', 'root', `-p${PASSWORD}`, '-B', '-N', '-D', db, '-e', query],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
} catch (e) {
if (allowError) return `ERROR: ${String(e.stderr || e.message).replace(/mysql: \[Warning\][^\n]*\n/, '').trim()}`;
throw e;
}
}
const commits = () => Number(sql('SELECT COUNT(*) FROM dolt_log'));
const head = () => sql("SELECT commit_hash FROM dolt_log LIMIT 1");
const say = (line = '') => console.log(line);
const json = (v) => { try { return JSON.parse(v); } catch { return v; } };
async function main() {
const client = await new MongoClient(URI).connect();
const db = client.db(DB);
await db.dropDatabase().catch(() => {});
// Setup the way WeKan has it: a board, two lists and some cards.
const boards = db.collection('boards');
const lists = db.collection('lists');
const cards = db.collection('cards');
await boards.insertOne({ _id: 'b1', title: 'Board', archived: false });
await lists.insertMany([{ _id: 'l1', boardId: 'b1', title: 'To do' }, { _id: 'l2', boardId: 'b1', title: 'Done' }]);
await cards.insertMany([
{ _id: 'c1', boardId: 'b1', listId: 'l1', title: 'Write docs', sort: 1, archived: false },
{ _id: 'c2', boardId: 'b1', listId: 'l1', title: 'Fix bug', sort: 2, archived: false },
]);
const table = sql(`SELECT table_name FROM information_schema.tables WHERE table_schema='${DB}' AND table_name LIKE 'cards%' LIMIT 1`);
if (TIMING_ONLY) {
const N = 200;
const t0 = process.hrtime.bigint();
for (let i = 0; i < N; i += 1) await cards.updateOne({ _id: 'c1' }, { $set: { title: `Edit ${i}` } });
const ms = Number(process.hrtime.bigint() - t0) / 1e6 / N;
say(`updateOne: ${ms.toFixed(2)} ms per call, ${commits()} commits, dolt_transaction_commit=${sql('SELECT @@dolt_transaction_commit', { db: 'information_schema' })}`);
await client.close();
return;
}
say('# 1. A Dolt commit per FerretDB write');
const ops = [
['edit title', () => cards.updateOne({ _id: 'c1' }, { $set: { title: 'Write docs v2' } })],
['move to Done', () => cards.updateOne({ _id: 'c1' }, { $set: { listId: 'l2', sort: 5 } })],
['archive (soft delete)', () => cards.updateOne({ _id: 'c1' }, { $set: { archived: true } })],
['hard delete c2', () => cards.deleteOne({ _id: 'c2' })],
];
const hashes = {};
for (const [name, run] of ops) {
const before = commits();
await run();
hashes[name] = head();
say(`${name.padEnd(24)} commits +${commits() - before} ${hashes[name].slice(0, 10)}`);
}
say(sql("SELECT CONCAT(committer, ' | ', date, ' | ', message) FROM dolt_log LIMIT 4"));
say('\n# 2. Who changed what: dolt_diff of the cards table, as WeKan\'s History table would show it');
const diff = sql(`SELECT diff_type, to_commit, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(from__ferretdb_sjson, '$.title')), ''), COALESCE(JSON_UNQUOTE(JSON_EXTRACT(to__ferretdb_sjson, '$.title')), ''), ` +
`COALESCE(JSON_UNQUOTE(JSON_EXTRACT(from__ferretdb_sjson, '$.listId')), ''), COALESCE(JSON_UNQUOTE(JSON_EXTRACT(to__ferretdb_sjson, '$.listId')), '') ` +
`FROM dolt_diff_${table} ORDER BY to_commit_date, to_commit`, { allowError: true });
say('diff_type | commit | title from -> to | list from -> to');
say(diff.split('\n').map((l) => { const c = l.split('\t'); return `${c[0]} | ${String(c[1]).slice(0, 8)} | ${c[2]} -> ${c[3]} | ${c[4]} -> ${c[5]}`; }).join('\n'));
say('\n# 3. Undo the last change (WeKan: Ctrl+Z) = DOLT_REVERT of HEAD, then redo = revert the revert');
const exists = async (id) => !!(await cards.findOne({ _id: id }));
say(`c2 exists before undo: ${await exists('c2')}`);
say(sql("CALL DOLT_REVERT('HEAD')", { allowError: true }));
say(`c2 exists after undo: ${await exists('c2')} (read through FerretDB, the MongoDB protocol)`);
say(sql("CALL DOLT_REVERT('HEAD')", { allowError: true }));
say(`c2 exists after redo: ${await exists('c2')}`);
say(`commits now: ${commits()} - an undo and a redo are commits of their own, the history is never rewritten`);
say('\n# 4. Restore ONE older change while later ones stay (WeKan: Restore of a selected History row)');
await cards.insertOne({ _id: 'c3', boardId: 'b1', listId: 'l1', title: 'Other card', sort: 3, archived: false });
await cards.updateOne({ _id: 'c3' }, { $set: { title: 'Other card v2' } });
const c3edit = head();
await cards.updateOne({ _id: 'c2x', boardId: 'b1' }, { $set: { title: 'x' } }, { upsert: true });
say('revert an old edit of c3 after an unrelated later commit (different row):');
say(sql(`CALL DOLT_REVERT('${c3edit}')`, { allowError: true }));
say(`c3 title: ${(await cards.findOne({ _id: 'c3' })).title}`);
say('revert the OLD title edit of c1 while c1 was changed again afterwards (same row, other field):');
say(sql(`CALL DOLT_REVERT('${hashes['edit title']}')`, { allowError: true }));
const c1 = await cards.findOne({ _id: 'c1' });
say(`c1 now: title=${c1.title} listId=${c1.listId} archived=${c1.archived}`);
say(`left behind by the failed revert: ${sql('SELECT COUNT(*) FROM dolt_conflicts', { allowError: true })} conflict table(s), status: ${sql("SELECT GROUP_CONCAT(CONCAT(table_name, ':', status)) FROM dolt_status", { allowError: true })}`);
const stuck = await cards.updateOne({ _id: 'c1' }, { $set: { sort: 99 } }).then(() => 'a write still works', (e) => `a write FAILS: ${String(e.message).slice(0, 120)}`);
say(`FerretDB write while the conflict is open: ${stuck}`);
say(`clean up with DOLT_RESET('--hard'): ${sql("CALL DOLT_RESET('--hard')", { allowError: true })}`);
const after = await cards.updateOne({ _id: 'c1' }, { $set: { sort: 100 } }).then(() => 'a write works again', (e) => `a write FAILS: ${String(e.message).slice(0, 120)}`);
say(`FerretDB write after the reset: ${after}`);
say('\n# 5. Time travel: the board as it was (AS OF), read with SQL');
const asOf = sql(`SELECT COUNT(*) FROM ${table} AS OF '${hashes['move to Done']}'`, { allowError: true });
const nowCount = sql(`SELECT COUNT(*) FROM ${table}`);
say(`cards at the "move to Done" commit: ${asOf}; now: ${nowCount}`);
const oldTitle = sql(`SELECT JSON_UNQUOTE(JSON_EXTRACT(_ferretdb_sjson, '$.title')) FROM ${table} AS OF '${hashes['edit title']}' WHERE JSON_UNQUOTE(JSON_EXTRACT(_ferretdb_sjson, '$._id')) = 'c1'`, { allowError: true });
say(`c1 title as of the "edit title" commit: ${oldTitle}`);
say('\n# 6. What it costs');
say(`commits for ${ops.length + 6} FerretDB writes: ${commits()}`);
say(`size of the data database: ${sql(`SELECT COUNT(*) FROM dolt_log`)} commits; chunks are shared, only changed rows are stored again`);
await client.close();
}
main().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,95 @@
'use strict';
// The same questions as dolt-history.cjs, asked of TerminusDB (a document and
// graph database with version control). TerminusDB speaks neither the MongoDB
// protocol nor SQL, so FerretDB cannot sit on it and WeKan cannot use it as it
// is; this only shows what its version control would give an undo and redo.
//
// docker run -d --name tdbtry -p 6363:6363 terminusdb/terminusdb-server:latest
// node docs/Databases/FerretDB/1/VersionHistory/terminusdb-history.cjs
const BASE = process.argv.includes('--url') ? process.argv[process.argv.indexOf('--url') + 1] : 'http://127.0.0.1:6363';
const AUTH = `Basic ${Buffer.from('admin:root').toString('base64')}`;
const DB = 'wekanhistory';
const say = (line = '') => console.log(line);
async function api(method, path, { query = {}, body, raw = false } = {}) {
const url = new URL(`${BASE}${path}`);
for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
const res = await fetch(url, {
method, headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
let data = text;
try { data = JSON.parse(text); } catch { try { data = text.trim().split('\n').map(JSON.parse); } catch { /* text */ } }
return raw ? { status: res.status, data } : { status: res.status, data };
}
const w = (author, message) => ({ author, message });
const short = (v) => JSON.stringify(v).slice(0, 220);
async function main() {
await api('DELETE', `/api/db/admin/${DB}`);
const created = await api('POST', `/api/db/admin/${DB}`, { body: { label: DB, comment: 'WeKan history test' } });
say(`create database: ${created.status}`);
const schema = [{
'@type': 'Class', '@id': 'Card', '@key': { '@type': 'Lexical', '@fields': ['cardId'] },
cardId: 'xsd:string', title: 'xsd:string', listId: 'xsd:string', sort: 'xsd:integer', archived: 'xsd:boolean',
}];
const s = await api('POST', `/api/document/admin/${DB}`, { query: { graph_type: 'schema', ...w('system', 'schema') }, body: schema });
say(`schema: ${s.status} ${s.status === 200 ? '' : short(s.data)}`);
const card = (id, title, listId, sort, archived = false) => ({ '@type': 'Card', cardId: id, title, listId, sort, archived });
const log = async () => (await api('GET', `/api/log/admin/${DB}`)).data;
say('\n# 1. A commit per write, with the author and message of the person who made it');
const ins = await api('POST', `/api/document/admin/${DB}`, { query: w('alice', 'add cards'), body: [card('c1', 'Write docs', 'l1', 1), card('c2', 'Fix bug', 'l1', 2)] });
say(`insert: ${ins.status} ${short(ins.data)}`);
const hashes = {};
const ops = [
['edit title', 'alice', () => api('PUT', `/api/document/admin/${DB}`, { query: w('alice', 'edit title'), body: card('c1', 'Write docs v2', 'l1', 1) })],
['move to Done', 'bob', () => api('PUT', `/api/document/admin/${DB}`, { query: w('bob', 'move to Done'), body: card('c1', 'Write docs v2', 'l2', 5) })],
['archive', 'carol', () => api('PUT', `/api/document/admin/${DB}`, { query: w('carol', 'archive'), body: card('c1', 'Write docs v2', 'l2', 5, true) })],
['hard delete c2', 'bob', () => api('DELETE', `/api/document/admin/${DB}`, { query: { ...w('bob', 'hard delete c2'), id: 'Card/c2' } })],
];
for (const [name, , run] of ops) {
const before = (await log()).length;
const r = await run();
const l = await log();
hashes[name] = l[0].identifier;
say(`${name.padEnd(16)} HTTP ${r.status} commits +${l.length - before} ${l[0].author} | ${l[0].message}`);
}
say((await log()).slice(0, 5).map((c) => `${c.author} | ${c.message} | ${new Date(c.timestamp * 1000).toISOString()} | ${String(c.identifier).slice(0, 10)}`).join('\n'));
say('\n# 2. Time travel: the cards as of an earlier commit');
const asOf = async (commit) => (await api('GET', `/api/document/admin/${DB}/local/commit/${commit}`, { query: { type: 'Card', as_list: 'true' } })).data;
const nowCards = (await api('GET', `/api/document/admin/${DB}`, { query: { type: 'Card', as_list: 'true' } })).data;
say(`now: ${nowCards.length} cards; as of "move to Done": ${short(await asOf(hashes['move to Done']))}`);
say('\n# 3. Undo the last change (WeKan: Ctrl+Z): a diff between two commits, applied backwards');
const lastTwo = (await log()).slice(0, 2);
const diffBack = await api('POST', `/api/diff/admin/${DB}`, { body: { before_data_version: lastTwo[0].identifier, after_data_version: lastTwo[1].identifier } });
say(`diff (hard delete -> before it): ${diffBack.status} ${short(diffBack.data)}`);
const patched = await api('POST', `/api/patch/admin/${DB}`, { body: { patch: diffBack.data, ...w('alice', 'undo: hard delete c2') } });
say(`patch: ${patched.status} ${short(patched.data)}`);
say(`c2 exists after undo: ${(await api('GET', `/api/document/admin/${DB}`, { query: { id: 'Card/c2' } })).status === 200}`);
say('\n# 4. Restore ONE older change while later ones stay (WeKan: Restore of a selected History row)');
// The "edit title" commit and the one before it.
const l = await log();
const idx = l.findIndex((c) => c.identifier === hashes['edit title']);
const titleBack = await api('POST', `/api/diff/admin/${DB}`, { body: { before_data_version: l[idx].identifier, after_data_version: l[idx + 1].identifier } });
say(`diff to undo the title edit: ${short(titleBack.data)}`);
const applied = await api('POST', `/api/patch/admin/${DB}`, { body: { patch: titleBack.data, ...w('alice', 'restore: title') } });
say(`patch onto HEAD, where c1 was moved and archived afterwards: ${applied.status} ${short(applied.data)}`);
const c1 = (await api('GET', `/api/document/admin/${DB}`, { query: { id: 'Card/c1' } })).data;
say(`c1 now: ${short(c1)}`);
say('\n# 5. What it costs');
const t0 = Date.now();
for (let i = 0; i < 100; i += 1) await api('PUT', `/api/document/admin/${DB}`, { query: w('alice', `edit ${i}`), body: card('c1', `Edit ${i}`, 'l2', 5) });
say(`PUT of a card: ${((Date.now() - t0) / 100).toFixed(1)} ms per call (an HTTP call each, a commit each); commits: ${(await log()).length}`);
}
main().catch((e) => { console.error(e); process.exit(1); });
+3 -3
View File
@@ -521,7 +521,7 @@ checked on the page itself.
does recursive search (`startWith`, `connectFromField`, `connectToField`,
`maxDepth`), spills to disk above 100 MB, returns results unsorted.
- **WeKan's FerretDB v1 fork does not implement `$graphLookup`** (verified in
source: `.tools/FerretDB/internal/handler/common/aggregations/stages/stages.go`
source: `.tools/w/FerretDB/internal/handler/common/aggregations/stages/stages.go`
`unsupportedStages`; `docs/reference/supported-commands.md`; `ROADMAP.md`).
Its `$lookup` is the basic equality join only. Upstream FerretDB v2's
[compatibility page](https://docs.ferretdb.io/migration/compatibility/) does
@@ -707,8 +707,8 @@ MongoDB documents the standard patterns in
| Closure table (separate collection of ancestor/descendant pairs) | 1 query | 1 query | many rows | heavy; SQL idiom |
`$graphLookup` is **not implemented** in WeKan's FerretDB v1 fork:
`.tools/FerretDB/internal/handler/common/aggregations/stages/stages.go` lists it
in `unsupportedStages`, and `.tools/FerretDB/ROADMAP.md` marks it
`.tools/w/FerretDB/internal/handler/common/aggregations/stages/stages.go` lists it
in `unsupportedStages`, and `.tools/w/FerretDB/ROADMAP.md` marks it
`ErrNotImplemented`. Since FerretDB on SQLite is a default WeKan database, any
design that needs `$graphLookup` would break there. **Array of ancestors** needs
only plain equality queries on an indexed array, which every backend supports.
+14 -2
View File
@@ -82,8 +82,20 @@ early.
## What the bar holds
In source order: the house and the page title, the logo, the phone/desktop
toggle, the drag-handles toggle, and then everything else.
In source order: the house and the page title, the logo, the full screen button,
the phone/desktop toggle, the drag-handles toggle, and then everything else.
**Full screen.** A button to the right of the logo puts the whole page in full
screen and takes it out again, with the Fullscreen API (`models/lib/fullscreen.js`):
the icon is an expand arrow, and a compress arrow while the page is in full screen,
and it follows the browser's own way out (Esc) too. Its label is the existing
"Maximize", and "Minimize" while the page is in full screen, so the same button
says how to leave (Esc and the browser's own control work too). It is drawn only where the browser offers the API and the page may use
it - iPad Safari, desktop browsers and Android browsers do; **iPhone Safari does not**
(the API is for video there) and neither does a page inside an `<iframe>` without
`allowfullscreen`, so no dead button is shown. On an iPhone, "Add to Home Screen"
opens WeKan without the browser's bars (the manifest says `display: standalone`).
It sits before the first icon the fold button collects, so folding the icons keeps it.
**The board's controls fold.** A caret leads the group, at its leading edge —
left of the lock in a left-to-right page, right of it in Arabic. Folded, the
@@ -43,3 +43,20 @@ For developers: the switch is `profile.keepAddCardFormOpen`, toggled by the
`client/components/lists/listBody.js` closes the form after its last title,
pinned by `tests/keepAddCardFormOpen6827.test.cjs` and
`tests/playwright/specs/keep-add-card-form-open-6827.e2e.js`.
## The focus outline
**Show focus outline** is personal and **on** by default. The blue ring round the
focused control is an accessibility feature (WCAG 2.4.7, Focus Visible): it tells
a keyboard user where they are, and it also shows after a click on a minicard,
which some members find distracting
([#6755](https://github.com/wekan/wekan/issues/6755)). Untick it and the ring - the
outline and its halo - is hidden for you, on every board; other members keep
theirs. Field borders and the colour a button takes on focus are not the ring and
stay. If you use the keyboard to move through WeKan, leave it on.
For developers: the switch is `profile.hideFocusOutline` (stored as the exception,
so an account that never touched it keeps the ring), toggled by the
`toggleHideFocusOutline` method; `client/components/main/uiFont.js` puts the class
`has-hide-focus-outline` on `<body>` and `client/components/main/layouts.css` turns
it into no ring. Pinned by `tests/hideFocusOutline6755.test.cjs`.
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-12.88-win64.zip](https://github.com/wekan/wekan/releases/download/v12.88/wekan-12.88-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
1. [wekan-13.01-win64.zip](https://github.com/wekan/wekan/releases/download/v13.01/wekan-13.01-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
2. [node.exe](https://nodejs.org/dist/latest-v26.x/win-x64/node.exe)
@@ -22,7 +22,7 @@ Right click and download files 1-4:
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-12.88-win64.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-13.01-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+1 -1
View File
@@ -518,7 +518,7 @@ Two ratchets keep that distinction reviewable. `tests/securityRegressionCoverage
accounts for every published Hall of Fame name and requires each named regression
suite to remain present. As of the 2026-09-05 audit, 71 of 93 have named coverage and
22 older fixes remain explicit test gaps. `tests/hallOfFameProblemsCoverage.test.cjs`
reads the real `.tools/wekan.fi` Hall of Fame and requires every name to be either a
reads the real `.tools/w/wekan.fi` Hall of Fame and requires every name to be either a
Problems catalog entry, a reasoned non-attributable case, or an explicit pending
review. A missing companion checkout may skip the website comparison, but the
obsolete `~/repos/w/wekan.fi` location is never read.
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL tautan logo login khusus",
"custom-help-link-url": "URL Tautan Bantu Khusuih",
"text-below-custom-login-logo": "Teks di miyup logo login khusus",
"login-background-color": "Warna latar halaman login",
"automatic-linked-url-schemes": "Skema URL khusus nyang haroh jeuet geuklik ngon otomatis. Saboh skema URL bak tiep bareh",
"automatic-linked-url-schemes-hint": "Bak atôran awai mantöng soh, jadi nyang jeuet teuklik cit tautan web ngon imèl. Tiep jeunéh tautan nyang na lam dapeuta, lagee thunderlink atawa onenote, jipeuhah aplikasi nyang ka teudaftar keu jeunéh nyan. javascript, data, vbscript ngon jeunéh nyang saban hana tom jeuet keu tautan.",
"external-link-pattern": "Tautan Keudroe u Peuturi Masalah Luwa",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Watèe judul kad barô geupeuphôn ngon [Haba] lheuh nyan na haba laén, Haba jeut keu label kad nyan (geupeugöt meunyo papan hana label ngon nan nyan) dan geupeugadoh nibak judul. Mate: judul teutap lagée nyan.",
"keep-add-card-form-open": "Peutinggai formulir Tamah Kad teuhah lheueh kad ka geutamah",
"keep-add-card-form-open-description": "Udep: lheueh kad ka geutamah, formulir teutap teuhah ngon judul soh keu kad lheueh nyoe. Mate: formulir teutôp meunan kad ka geutamah.",
"show-focus-outline": "Peuĝah garis fokus",
"checklist-ding-sound": "Peubunyi su watèe item checklist geuseuleusoe",
"checklist-ding-sound-description": "Peubunyi su \"ding\" paneuk watèe droeneueh cek saboh item checklist. Matê meunurôt default.",
"assignee": "Ureueng nyang geubri buet",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Koneksi gagai",
"cloud-settings-saved": "Peuatôran storan ka geukeubah",
"cloud-settings-save-failed": "Gagai keubah peuatôran storan",
"cloud": "Awan",
"cloud-settings-desc": "Ngon WeKan lam Weddy, Cloud Panel jipeulayan le that domain WeKan nibak saboh server, tiep-tiep ngon data droe. Bak Domain Cloud Panel, SuperAdmin geu-urôh domain ngon CloudFlare.",
"cloud-panel-domain": "Domain Cloud Panel",
"cloud-superadmin-username": "Nan Ureueng Ngui SuperAdmin",
"cloud-saved": "Ka teukeubah.",
"cloud-panel-not-allowed": "Cloud Panel cit keu SuperAdmin, bak Domain Cloud Panel.",
"cloud-customer-name": "Nan pelanggan",
"cloud-customer-email": "Alamat emel pelanggan",
"cloud-billing-email": "Alamat emel tagihan",
"cloud-start-date": "Tanggai Phôn",
"cloud-end-date": "Tanggai Akhe",
"cloudflare-api-key": "Kunci API CloudFlare",
"cloudflare-api-key-desc": "Ngon kunci API CloudFlare, domain nyang zona jih na lam akun CloudFlare droeneuh meuteumèe rekod DNS proxy ngon sertifikat Origin CloudFlare. Domain laen meuteumèe sertifikat Let's Encrypt. Kunci geukeubah bak server ngon hana geupeuleumah le.",
"cloudflare-api-key-set": "Kunci API CloudFlare ka geukeubah.",
"cloudflare-api-key-not-set": "Hana kunci API CloudFlare nyang geukeubah: bandum domain meuteumèe sertifikat Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Pindah lampiran u Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Gepasmaakte aanmeldlogo-skakel-URL",
"custom-help-link-url": "Gepasmaakte hulpskakel-URL",
"text-below-custom-login-logo": "Teks onder Custom Login Logo",
"login-background-color": "Agtergrondkleur van aanmeldblad",
"automatic-linked-url-schemes": "Pasgemaakte URL-skemas wat outomaties klikbaar moet wees. Een URL-skema per reël",
"automatic-linked-url-schemes-hint": "By verstek leeg, sodat slegs web- en posskakels klikbaar is. Elke gelyste skema, byvoorbeeld thunderlink of onenote, open die toepassing wat daarvoor geregistreer is. javascript, data, vbscript en soortgelyke skemas word nooit skakels nie.",
"external-link-pattern": "Outomatiese skakel na eksterne kwessie-opspoorder",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Wanneer 'n nuwe kaart se titel begin met [Teks] gevolg deur meer teks, word Teks 'n etiket van die kaart (geskep as die raad nog geen etiket met daardie naam het nie) en uit die titel verwyder. Af: die titel bly soos dit is.",
"keep-add-card-form-open": "Hou die Voeg kaart by-vorm oop nadat 'n kaart bygevoeg is",
"keep-add-card-form-open-description": "Aan: nadat 'n kaart bygevoeg is, bly die vorm oop met 'n leë titel vir die volgende kaart. Af: die vorm maak toe sodra die kaart bygevoeg is.",
"show-focus-outline": "Wys fokusomlyn",
"checklist-ding-sound": "Speel 'n klank wanneer 'n kontrolelys-item voltooi word",
"checklist-ding-sound-description": "Speel 'n kort klokkie wanneer jy 'n kontrolelysitem afmerk. Standaard af.",
"assignee": "Gevolmagtigde",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Verbinding het misluk",
"cloud-settings-saved": "Bergingsinstellings gestoor",
"cloud-settings-save-failed": "Kon nie bergingsinstellings stoor nie",
"cloud": "Wolk",
"cloud-settings-desc": "Met WeKan in Weddy bedien die Cloud Panel baie WeKan-domeine vanaf een bediener, elk met sy eie data. By die Cloud Panel-domein bestuur die SuperAdmin die domeine en CloudFlare.",
"cloud-panel-domain": "Cloud Panel-domein",
"cloud-superadmin-username": "SuperAdmin-gebruikersnaam",
"cloud-saved": "Gestoor.",
"cloud-panel-not-allowed": "Die Cloud Panel is slegs vir die SuperAdmin, by die Cloud Panel-domein.",
"cloud-customer-name": "Kliënt se naam",
"cloud-customer-email": "Kliënt se e-posadres",
"cloud-billing-email": "Faktuur-e-posadres",
"cloud-start-date": "Begindatum",
"cloud-end-date": "Einddatum",
"cloudflare-api-key": "CloudFlare API-sleutel",
"cloudflare-api-key-desc": "Met 'n CloudFlare API-sleutel kry 'n domein waarvan die sone in jou CloudFlare-rekening is 'n geproksiede DNS-rekord en 'n CloudFlare Origin-sertifikaat. Ander domeine kry 'n Let's Encrypt-sertifikaat. Die sleutel word op die bediener gestoor en word nooit weer gewys nie.",
"cloudflare-api-key-set": "'n CloudFlare API-sleutel is gestoor.",
"cloudflare-api-key-not-set": "Geen CloudFlare API-sleutel is gestoor nie: elke domein kry 'n Let's Encrypt-sertifikaat.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Skuif aanhangsel na Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Gepasmaakte aanmeldlogo-skakel-URL",
"custom-help-link-url": "Gepasmaakte hulpskakel-URL",
"text-below-custom-login-logo": "Teks onder Custom Login Logo",
"login-background-color": "Agtergrondkleur van aanmeldblad",
"automatic-linked-url-schemes": "Pasgemaakte URL-skemas wat outomaties klikbaar moet wees. Een URL-skema per reël",
"automatic-linked-url-schemes-hint": "By verstek leeg, sodat slegs web- en posskakels klikbaar is. Elke gelyste skema, byvoorbeeld thunderlink of onenote, open die toepassing wat daarvoor geregistreer is. javascript, data, vbscript en soortgelyke skemas word nooit skakels nie.",
"external-link-pattern": "Outomatiese skakel na eksterne kwessie-opspoorder",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Wanneer 'n nuwe kaart se titel begin met [Teks] gevolg deur meer teks, word Teks 'n etiket van die kaart (geskep as die raad nog geen etiket met daardie naam het nie) en uit die titel verwyder. Af: die titel bly soos dit is.",
"keep-add-card-form-open": "Hou die Voeg kaart by-vorm oop nadat 'n kaart bygevoeg is",
"keep-add-card-form-open-description": "Aan: nadat 'n kaart bygevoeg is, bly die vorm oop met 'n leë titel vir die volgende kaart. Af: die vorm maak toe sodra die kaart bygevoeg is.",
"show-focus-outline": "Wys fokusomlyn",
"checklist-ding-sound": "Speel 'n klank wanneer 'n kontrolelys-item voltooi word",
"checklist-ding-sound-description": "Speel 'n kort klokkie wanneer jy 'n kontrolelysitem afmerk. Standaard af.",
"assignee": "Gevolmagtigde",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Verbinding het misluk",
"cloud-settings-saved": "Bergingsinstellings gestoor",
"cloud-settings-save-failed": "Kon nie bergingsinstellings stoor nie",
"cloud": "Wolk",
"cloud-settings-desc": "Met WeKan in Weddy bedien die Cloud Panel baie WeKan-domeine vanaf een bediener, elk met sy eie data. By die Cloud Panel-domein bestuur die SuperAdmin die domeine en CloudFlare.",
"cloud-panel-domain": "Cloud Panel-domein",
"cloud-superadmin-username": "SuperAdmin-gebruikersnaam",
"cloud-saved": "Gestoor.",
"cloud-panel-not-allowed": "Die Cloud Panel is slegs vir die SuperAdmin, by die Cloud Panel-domein.",
"cloud-customer-name": "Kliënt se naam",
"cloud-customer-email": "Kliënt se e-posadres",
"cloud-billing-email": "Faktuur-e-posadres",
"cloud-start-date": "Begindatum",
"cloud-end-date": "Einddatum",
"cloudflare-api-key": "CloudFlare API-sleutel",
"cloudflare-api-key-desc": "Met 'n CloudFlare API-sleutel kry 'n domein waarvan die sone in jou CloudFlare-rekening is 'n geproksiede DNS-rekord en 'n CloudFlare Origin-sertifikaat. Ander domeine kry 'n Let's Encrypt-sertifikaat. Die sleutel word op die bediener gestoor en word nooit weer gewys nie.",
"cloudflare-api-key-set": "'n CloudFlare API-sleutel is gestoor.",
"cloudflare-api-key-not-set": "Geen CloudFlare API-sleutel is gestoor nie: elke domein kry 'n Let's Encrypt-sertifikaat.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Skuif aanhangsel na Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Wo ankasa kɔ-mu agyiraehyɛde link URL",
"custom-help-link-url": "W’ankasa mmoa link URL",
"text-below-custom-login-logo": "Nsɛm a ɛwɔ wo ankasa kɔ-mu agyiraehyɛde no ase",
"login-background-color": "Login kratafa no akyi ahosu",
"automatic-linked-url-schemes": "URL akwan a woahyɛ da ayɛ a ɛsɛ sɛ wotumi mia so ankasa. URL kwan biako wɔ nkyerɛw no ɔfa biara so",
"automatic-linked-url-schemes-hint": "Mfitiaseɛ no, ɛyɛ hunu, enti wɛb ne ɛmɛl nkitahodie nko ara na wobɛtumi aklika so. Scheme biara a ɛwɔ din no mu, sɛ nhwɛsoɔ no thunderlink anaa onenote, bue app a wɔakyerɛw ne din ama no no. Wɔnyɛ javascript, data, vbscript ne scheme a ɛte saa no nkitahodie da.",
"external-link-pattern": "Ntoatoaso ahomaso a ɛkɔ nsɛnnennennee foforo so",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Sɛ kaad foforɔ asɛmti fi ase de [Nkyerɛwee] na nkyerɛwee foforɔ di akyi a, Nkyerɛwee no bɛyɛ kaad no nsɛnkyerɛnne (wɔbɛyɛ no sɛ bɔɔd no nni nsɛnkyerɛnne a ne din saa) na wɔayi afi asɛmti no mu. Mmom: asɛmti no tena sɛdeɛ ɛte.",
"keep-add-card-form-open": "Ma Fa kaad ka ho krataa no nkɔ so mmue wɔ kaad bi a wode aka ho akyi",
"keep-add-card-form-open-description": "Bue: sɛ wode kaad aka ho a, krataa no kɔ so bue a ne din yɛ hwee ma kaad a edi hɔ. To mu: krataa no to mu bere a wode kaad no aka ho.",
"show-focus-outline": "Kyerɛ ahwɛyie ntwerɛ",
"checklist-ding-sound": "Bɔ nnyigyei bere a wɔawie nhwehwɛmu no mu ade bi",
"checklist-ding-sound-description": "Bɔ nnyigyei tiaa bi bere a wohyɛ nhwehwɛmu no mu ade bi agyirae sɛ wɔawie. Wɔadum fi mfiase.",
"assignee": "Nea wɔde adwuma ama no",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Mfomso bi sii wɔ dwumadi yi mu",
"cloud-settings-saved": "Wɔasie akorabea nhyehyɛe no",
"cloud-settings-save-failed": "Wɔantumi ansie akorabea nhyehyɛe no",
"cloud": "Mununkum",
"cloud-settings-desc": "Sɛ WeKan wɔ Weddy mu a, Cloud Panel no som WeKan domain pii fi server baako so, na emu biara wɔ ne data. Wɔ Cloud Panel Domain no so, SuperAdmin no hwɛ domain no ne CloudFlare so.",
"cloud-panel-domain": "Cloud Panel Domain din",
"cloud-superadmin-username": "SuperAdmin dwumadini din",
"cloud-saved": "Wɔasie.",
"cloud-panel-not-allowed": "Cloud Panel no yɛ SuperAdmin nko ara dea, wɔ Cloud Panel Domain no so.",
"cloud-customer-name": "Adetɔfo din",
"cloud-customer-email": "Adetɔfo email address",
"cloud-billing-email": "Ka tua email address",
"cloud-start-date": "Mfitiaseɛ da",
"cloud-end-date": "Awieeɛ da",
"cloudflare-api-key": "CloudFlare API safe",
"cloudflare-api-key-desc": "Sɛ wowɔ CloudFlare API safe a, domain a ne zone wɔ wo CloudFlare account mu no nya DNS record a ɛfa proxy so ne CloudFlare Origin krataa. Domain afoforo nya Let's Encrypt krataa. Wɔde safe no sie server no so na wɔnkyerɛ bio da.",
"cloudflare-api-key-set": "Wɔakora CloudFlare API safe.",
"cloudflare-api-key-not-set": "Wɔnkoraa CloudFlare API safe biara: domain biara nya Let's Encrypt krataa.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Fa fael a wɔde aka ho no kɔ Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "ብጁ የመግቢያ አርማ አገናኝ URL",
"custom-help-link-url": "ብጁ የእገዛ አገናኝ URL",
"text-below-custom-login-logo": "ከብጁ የመግቢያ አርማ በታች ያለ ጽሑፍ",
"login-background-color": "የመግቢያ ገጽ የጀርባ ቀለም",
"automatic-linked-url-schemes": "በራስ-ሰር ሊጫኑ የሚገባቸው ብጁ የURL መርሃግብሮች። በእያንዳንዱ መስመር አንድ የURL መርሃግብር",
"automatic-linked-url-schemes-hint": "በነባሪ ባዶ ነው፣ ስለዚህ የድር እና የኢሜይል አገናኞች ብቻ ጠቅ ሊደረጉ ይችላሉ። እያንዳንዱ የተዘረዘረ ስኪማ፣ ለምሳሌ thunderlink ወይም onenote፣ ለእሱ የተመዘገበውን መተግበሪያ ይከፍታል። javascript፣ data፣ vbscript እና ተመሳሳይ ስኪማዎች ፈጽሞ አገናኝ አይሆኑም።",
"external-link-pattern": "ወደ ውጫዊ ችግር መከታተያ ራስ-ሰር አገናኝ",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "የአዲስ ካርድ ርዕስ በ[ጽሑፍ] ተጀምሮ ሌላ ጽሑፍ ከተከተለው፣ ጽሑፍ የካርዱ መለያ ይሆናል (ሰሌዳው በዚያ ስም መለያ ከሌለው ይፈጠራል) እና ከርዕሱ ይወገዳል። ጠፍቷል፦ ርዕሱ እንዳለ ይቆያል።",
"keep-add-card-form-open": "ካርድ ከታከለ በኋላ የካርድ ጨምር ቅጹ ክፍት ሆኖ ይቆይ",
"keep-add-card-form-open-description": "በርቷል: ካርድ ከታከለ በኋላ ቅጹ ለቀጣዩ ካርድ ባዶ ርዕስ ይዞ ክፍት ሆኖ ይቆያል። ጠፍቷል: ካርዱ እንደታከለ ቅጹ ይዘጋል።",
"show-focus-outline": "የትኩረት ዙሪያ መስመር አሳይ",
"checklist-ding-sound": "የማጣሪያ ዝርዝር ንጥል ሲጠናቀቅ ድምፅ አጫውት",
"checklist-ding-sound-description": "የማጣሪያ ዝርዝር ንጥል ሲያመልክቱ አጭር ድምፅ ያጫውቱ። ነባሪው ጠፍቷል።",
"assignee": "ኃላፊ",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "ግንኙነቱ አልተሳካም",
"cloud-settings-saved": "የማከማቻ ቅንብሮች ተቀምጠዋል",
"cloud-settings-save-failed": "የማከማቻ ቅንብሮችን ማስቀመጥ አልተሳካም",
"cloud": "ደመና",
"cloud-settings-desc": "WeKan በWeddy ውስጥ ሲሆን፣ Cloud Panel ብዙ የWeKan ጎራዎችን ከአንድ አገልጋይ ያገለግላል፣ እያንዳንዱም የራሱ ውሂብ አለው። በCloud Panel ጎራ ላይ SuperAdmin ጎራዎቹንና CloudFlareን ያስተዳድራል።",
"cloud-panel-domain": "የCloud Panel ጎራ",
"cloud-superadmin-username": "የSuperAdmin የተጠቃሚ ስም",
"cloud-saved": "ተቀምጧል።",
"cloud-panel-not-allowed": "Cloud Panel ለSuperAdmin ብቻ ነው፣ በCloud Panel ጎራ ላይ።",
"cloud-customer-name": "የደንበኛ ስም",
"cloud-customer-email": "የደንበኛ ኢሜይል አድራሻ",
"cloud-billing-email": "የክፍያ ኢሜይል አድራሻ",
"cloud-start-date": "የመጀመሪያ ቀን",
"cloud-end-date": "የማብቂያ ቀን",
"cloudflare-api-key": "የCloudFlare API ቁልፍ",
"cloudflare-api-key-desc": "በCloudFlare API ቁልፍ፣ ዞኑ በCloudFlare መለያዎ ውስጥ ያለ ጎራ በፕሮክሲ የሚያልፍ የDNS መዝገብ እና የCloudFlare Origin የምስክር ወረቀት ያገኛል። ሌሎች ጎራዎች የLet's Encrypt የምስክር ወረቀት ያገኛሉ። ቁልፉ በአገልጋዩ ላይ ይቀመጣል እና ዳግም አይታይም።",
"cloudflare-api-key-set": "የCloudFlare API ቁልፍ ተቀምጧል።",
"cloudflare-api-key-not-set": "ምንም የCloudFlare API ቁልፍ አልተቀመጠም፦ እያንዳንዱ ጎራ የLet's Encrypt የምስክር ወረቀት ያገኛል።",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "አባሪውን ወደ Azure Blob Storage አንቀሳቅስ",
+217 -200
View File
@@ -227,20 +227,20 @@
"attachment-soft-delete-pop": "L'adchunto se saca d'esta tarcheta y o suyo fichero se conserva. Qualsequiera que pueda editar a tarcheta puede restaurar-lo dende l'historial d'a tarcheta.",
"attachmentDeletePopup-title": "¿Borrar el adjunto?",
"attachments": "Adchuntos",
"attach-card": "Attach card",
"attachCardPopup-title": "Attach card",
"attach-card-hint": "Paste a card link (Copy card link on the other card), or type a card title on this board",
"attached-card-unavailable": "An attached card you cannot see",
"detach-card": "Remove attached card",
"attach-card-self": "A card cannot be attached to itself",
"attach-card-limit": "This card already has as many attached cards as it can have",
"attach-card-not-found": "No card you can see was found for that link",
"attach-card": "Adchuntar tarcheta",
"attachCardPopup-title": "Adchuntar tarcheta",
"attach-card-hint": "Apega un vinclo de tarcheta (Copiar vinclo d'a tarcheta en l'atra tarcheta), u escribe un titol de tarcheta d'este tablero",
"attached-card-unavailable": "Una tarcheta adchunta que no puetz veyer",
"detach-card": "Sacar a tarcheta adchunta",
"attach-card-self": "Una tarcheta no se puet adchuntar a ella mesma",
"attach-card-limit": "Esta tarcheta ya tiene tantas tarchetas adchuntas como puede tener",
"attach-card-not-found": "No s'ha trobau garra tarcheta que puedas veyer pa ixe vinclo",
"auto-watch": "Suscribir-se automaticament a los tableros quan se crean",
"avatar-too-big": "L’avatar ye masiau gran (__size__ como maximo)",
"back": "Dezaga",
"board-change-color": "Cambiar el color",
"board-announcement": "Board announcement",
"board-announcement-enabled": "Show it to everyone on this board",
"board-announcement": "Anuncio d'o tablero",
"board-announcement-enabled": "Amostrar-lo a totz en este tablero",
"board-change-background-image": "Cambiar imagen de fondo",
"board-background-image-url": "URL d’a imachen de fondo",
"add-background-image": "Adhibir imagen de fondo",
@@ -284,7 +284,7 @@
"zoom-level": "Nivel de zoom",
"enter-zoom-level": "Introduz o livel de zoom (50-300%):",
"board-view-cal": "Calendario",
"board-view-calendar-mode": "Calendar Mode",
"board-view-calendar-mode": "Modo calendario",
"board-view-multiboard-cal": "Calendario (Toz os tableros)",
"board-view-swimlanes": "Carrils",
"board-view-collapse": "Contraer",
@@ -686,32 +686,32 @@
"chooseBoardSourcePopup-title": "Importar un tablero",
"createLabelPopup-title": "Crear una etiqueta",
"createCustomField": "Crear un campo",
"custom-field-links": "Link fields to another card",
"custom-field-links-hint": "Custom fields with the same name and type on both cards are linked: when a value changes on one card, it is copied to the other card. Fields that only one card has are left alone.",
"custom-field-links-none": "This card's custom fields are not linked to any card.",
"custom-field-link-unavailable": "A linked card you cannot see",
"custom-field-link-fields": "Linked fields:",
"custom-field-link-no-fields": "No fields with the same name and type yet",
"custom-field-link-inactive": "Paused: the person who made this link can no longer edit both cards, or a card is archived.",
"custom-field-unlink": "Unlink",
"custom-field-link-target": "Card link (Copy card link), or search this board's cards",
"custom-field-link-direction": "Direction",
"custom-field-link-both": "Both ways: changes on either card go to the other",
"custom-field-link-send": "One way: changes on this card go to the main card",
"custom-field-link-sends": "Changes on this card go to that card",
"custom-field-link-receives": "Changes on that card come to this card",
"custom-field-link-add": "Link fields",
"field-link-self": "A card cannot be linked to itself.",
"field-link-limit": "This card, or that card, already has the most linked cards it can have.",
"field-link-archived": "An archived card cannot be linked.",
"field-link-invalid": "That is not a card link.",
"field-link-not-found": "Card not found.",
"field-link-not-allowed": "You need to be able to edit both cards.",
"cardCustomFieldLinksPopup-title": "Linked custom fields",
"custom-field-links": "Vincular campos a una atra tarcheta",
"custom-field-links-hint": "Os campos personalizaus con o mesmo nombre y tipo en as dos tarchetas son vinculaus: quan una valor cambia en una tarcheta, se copia en l'atra. Os campos que nomás tiene una tarcheta no se tocan.",
"custom-field-links-none": "Os campos personalizaus d'esta tarcheta no son vinculaus a garra tarcheta.",
"custom-field-link-unavailable": "Una tarcheta vinculada que no puetz veyer",
"custom-field-link-fields": "Campos vinculaus:",
"custom-field-link-no-fields": "Encara no i hai campos con o mesmo nombre y tipo",
"custom-field-link-inactive": "En pausa: a persona que fació este vinclo ya no puet editar as dos tarchetas, u una tarcheta ye archivada.",
"custom-field-unlink": "Desvincular",
"custom-field-link-target": "Vinclo de tarcheta (Copiar vinclo d'a tarcheta), u mira entre as tarchetas d'este tablero",
"custom-field-link-direction": "Adreza",
"custom-field-link-both": "En as dos adrezas: os cambios en qualsequier tarcheta van ta l'atra",
"custom-field-link-send": "En una adreza: os cambios en esta tarcheta van ta la tarcheta principal",
"custom-field-link-sends": "Os cambios en esta tarcheta van ta ixa tarcheta",
"custom-field-link-receives": "Os cambios en ixa tarcheta plegan ta esta tarcheta",
"custom-field-link-add": "Vincular campos",
"field-link-self": "Una tarcheta no se puet vincular a ella mesma.",
"field-link-limit": "Esta tarcheta, u ixa tarcheta, ya tiene o maximo de tarchetas vinculadas que puede tener.",
"field-link-archived": "Una tarcheta archivada no se puet vincular.",
"field-link-invalid": "Ixo no ye un vinclo de tarcheta.",
"field-link-not-found": "No s'ha trobau a tarcheta.",
"field-link-not-allowed": "Has de poder editar as dos tarchetas.",
"cardCustomFieldLinksPopup-title": "Campos personalizaus vinculaus",
"createCustomFieldPopup-title": "Crear un campo",
"current": "actual",
"custom-color": "Color personalizado",
"cards-use-list-color": "Cards without a colour of their own use this colour",
"cards-use-list-color": "As tarchetas sin color propia fan servir esta color",
"custom-field-delete-pop": "Se borrará iste campo personalizado de todas as tarchetas y se destruirá su historial. Ista acción no puet deshacerse.",
"custom-field-checkbox": "Casilla de verificación",
"custom-field-currency": "Moneda",
@@ -909,20 +909,20 @@
"import-board-instruction-zenkit": "Apega un JSON d'exportación de lista de ZenKit ({ \"title\", \"stages\":[...], \"items\":[...] }). As etapas se tornan listas y os elementos se tornan tarchetas.",
"import-board-instruction-markdown": "Apega una lista de quefers markdown (a convención que emplegan ferramientas markdown-kanban como Obsidian Kanban): capiters \"## Nombre de lista\", con elementos \"- [ ]\"/\"- [x]\" debaixo. Os capiters se tornan listas y os elementos se tornan tarchetas; una lista simpla con pinchos sin caixetas de selección tamién s'importa, como tarchetas ubiertas.",
"import-board-instruction-leo": "Apega un esquema Leo (fichero .leo). Os nodos de primer nivel se tornan listas, os suyos fillos se tornan tarchetas con o cuerpo d'o nodo como descripción, y os nodos más fundos se tornan listas de comprebación. Os nodos marcaus s'importan como feitos.",
"import-board-instruction-opml": "Paste an OPML outline, as Workflowy, Dynalist, OmniOutliner, Logseq and other outliners export it. Top-level items become lists, their children become cards with the note as the description, and deeper items become checklists. Completed items import as done.",
"import-board-instruction-orgmode": "Paste an Org mode file, as Emacs, Orgzly or Beorg write it. Top-level headings become lists and second-level headings become cards. TODO and DONE, priorities, tags, SCHEDULED, DEADLINE and CLOSED are kept, and checkboxes and deeper headings become checklists.",
"import-board-instruction-opml": "Apega un esquema OPML, tal como l'exportan Workflowy, Dynalist, OmniOutliner, Logseq y atros editors d'esquemas. Os elementos d'o primer nivel se tornan listas, os suyos fillos se tornan tarchetas con a nota como descripción, y os elementos mas fondos se tornan listas de comprebación. Os elementos completaus s'importan como feitos.",
"import-board-instruction-orgmode": "Apega un fichero Org mode, tal como lo escriben Emacs, Orgzly u Beorg. Os titols d'o primer nivel se tornan listas y os d'o segundo nivel se tornan tarchetas. TODO y DONE, prioridatz, etiquetas, SCHEDULED, DEADLINE y CLOSED se conservan, y as caixetas de verificación y os titols mas fondos se tornan listas de comprebación.",
"import-board-instruction-todotxt": "Apega un fichero todo.txt: un quefer por linia, tal como lo escriben l'aplicación todo.txt y as aplicacions compatibles. Os quefers remataus (\"x\") van ta Feito, +project y @context se tornan etiquetas, (A) se torna una etiqueta de prioridat, y due: y t: se tornan datas de vencimiento y d'inicio.",
"import-board-instruction-taskwarrior": "Apega a salida de task export: una matriz JSON u una fayena por linia. As fayenas completadas van ta Done, project y priority se convierten en etiquetas, annotations en comentarios y depends en dependencias d’as fayenas que las blocan.",
"import-board-instruction-focalboard": "Descomprime o fichero .boardarchive y apega o conteniu d’o board.jsonl d’o tablero, con u sin a linia de versión. A propiedat d’agrupación d’a vista d’o tablero se convierte en listas, as atras propiedatz de selección en etiquetas, os bloques de texto y de caixetas de verificación en a descripción y una lista de comprebación, y os comentarios se conservan. As imachens y os adchuntos no s’importan.",
"import-board-instruction-todoist": "In Todoist, open the project menu and choose Export as a template, then paste the CSV file. Sections become lists, tasks become cards, sub-tasks become a checklist, notes become comments, and @labels and priorities p1 to p3 become labels.",
"import-board-instruction-planner": "In Microsoft Planner, open the plan's menu and choose Export plan to Excel, then choose that .xlsx file. Buckets become lists and tasks become cards, with their assignees, dates, labels, checklist and description; Progress, Priority and Completed By become custom fields.",
"import-board-instruction-meistertask": "In MeisterTask, click the project name, choose Export project and export the CSV, then paste it. Sections become lists and tasks become cards, with their notes, due date, assignee and tags; completed tasks keep their completion date. A CSV written for MeisterTask's own import works too.",
"import-board-instruction-obsidian": "In Obsidian, open the Kanban board's file as Markdown (or open the .md file in a text editor), then paste all of it. Lanes become lists with their card limits, cards keep their tags, dates, priority, fields and checklist, and the Archive becomes archived cards.",
"import-board-instruction-linear": "In Linear, export the issues as CSV (Settings, Import / Export, Export data, or a view's export), then paste the CSV. Statuses become lists, teams become swimlanes, and issues become cards with their assignee, labels, dates, parent issue, priority, estimate, project and cycle.",
"import-board-instruction-ticktick": "In TickTick, open Settings, Account, Backup & Import and generate a backup, then paste the CSV file. Each TickTick list becomes a swimlane and its columns become lists; tasks keep their tags, dates, checklist, priority and subtasks.",
"import-board-instruction-clickup": "In ClickUp, export the tasks as CSV (Settings, Imports / Exports, Export Items, or a List or Table view's export with all columns), then paste the CSV. Statuses become lists, ClickUp lists become swimlanes, and tasks become cards with their subtasks, assignees, tags, dates, priority and time.",
"import-board-instruction-nullboard": "In Nullboard, open the board, choose Export this board... from the menu, open the .nbx file in a text editor and paste its contents. Lists become lists and notes become cards: a note's first line is the card title and the rest its description. A raw note gets the label raw. A file with several boards imports its first board.",
"import-board-instruction-kanri": "In Kanri, open Import & Export and choose Export individual board (or Export all data), then paste the .json file. Columns become lists and cards become cards, with their description, due date, tags and colors; a card's tasks become a checklist. From an all-data export the first board is imported.",
"import-board-instruction-meistertask": "En MeisterTask, fe clic en o nombre d'o prochecto, tría Export project y exporta o CSV, dimpués apega-lo. As seccions se tornan listas y as tareas se tornan tarchetas, con as suyas notas, calendata de vencimiento, asignau y etiquetas; as tareas completadas conservan a suya calendata de remate. Un CSV escrito pa la importación propia de MeisterTask tamién funciona.",
"import-board-instruction-obsidian": "En Obsidian, ubre o fichero d'o tablero Kanban como Markdown (u ubre o fichero .md en un editor de texto), y dimpués apega-lo tot. Os carrils se tornan listas con os suyos limites de tarchetas, as tarchetas conservan as suyas etiquetas, calendatas, prioridat, campos y lista de comprebación, y l'Archive se torna tarchetas archivadas.",
"import-board-instruction-linear": "En Linear, exporta as incidencias como CSV (Settings, Import / Export, Export data, u a exportación d'una vista), dimpués apega o CSV. Os estaus se tornan listas, os equipos se tornan carrils, y as incidencias se tornan tarchetas con o suyo asignau, etiquetas, calendatas, incidencia pai, prioridat, estimación, prochecto y cicle.",
"import-board-instruction-ticktick": "En TickTick, ubre Settings, Account, Backup & Import y chenera una copia de seguranza, dimpués apega o fichero CSV. Cada lista de TickTick se torna un carril y as suyas columnas se tornan listas; as tareas conservan as suyas etiquetas, calendatas, lista de comprebación, prioridat y subtareas.",
"import-board-instruction-clickup": "En ClickUp, exporta as tareas como CSV (Settings, Imports / Exports, Export Items, u a exportación d'una vista List u Table con todas as columnas), dimpués apega o CSV. Os estaus se tornan listas, as listas de ClickUp se tornan carrils, y as tareas se tornan tarchetas con as suyas subtareas, asignaus, etiquetas, calendatas, prioridat y tiempo.",
"import-board-instruction-nullboard": "En Nullboard, ubre o tablero, tría Export this board... d'o menú, ubre o fichero .nbx en un editor de texto y apega o suyo conteniu. As listas se tornan listas y as notas se tornan tarchetas: a primer linia d'una nota ye o titol d'a tarcheta y a resta a suya descripción. Una nota en cruo recibe l'etiqueta raw. D'un fichero con quantos tableros s'importa o primer tablero.",
"import-board-instruction-kanri": "En Kanri, ubre Import & Export y tría Export individual board (u Export all data), dimpués apega o fichero .json. As columnas se tornan listas y as tarchetas se tornan tarchetas, con a suya descripción, calendata de vencimiento, etiquetas y colors; as tareas d'una tarcheta se tornan una lista de comprebación. D'una exportación de totz os datos s'importa o primer tablero.",
"import-board-instruction-planka": "Escribe l'adreza d'o tuyo Planka (per eixemplo https://planka.example.com), o tuyo nombre d'usuario y clau de paso de Planka, y dimpués preta Listar. Nomás s'usan pa recullir o tablero y no se alzan. Tría un tablero y preta Recullir: WeKan lo leye dende l'API de Planka, con as suyas listas, tarchetas, descripcions, calendatas de vencimiento, etiquetas, miembros, listas de fayenas y comentarios, y dimpués contina a importación como de costumbre. As tarchetas de l'archivo de Planka s'importan archivadas; os adchuntos y as tarchetas d'a paperera no s'importan.",
"import-board-instruction-mstodo": "En Microsoft Graph Explorer (developer.microsoft.com/graph/graph-explorer), dentra, consiente o permiso Tasks.Read y copia o token d'a pestanya Access token. Apega-lo aquí y fe clic en Listar; o token nomás s'usa pa esta recullida y no s'alza. Tría totas as listas u una lista y fe clic en Recullir. Cada lista de To Do se fa una lista y cada fayena una tarcheta, con as suyas notas, calendatas de vencimiento y d'inicio, os pasos como lista de comprebación, as categorías como etiquetas, y l'importancia alta y l'estau como etiquetas; as fayenas completadas s'archivan. Os recordatorios y repeticions no s'importan.",
"import-board-instruction-kanbanflow": "En KanbanFlow, ubre o menú d'o tablero, tría Settings → API & Webhooks y copia o token d'API d'o tablero. Apega o token aquí, fe clic en Listar, tría o tablero y fe clic en Recullir; o token nomás s'usa pa ixa recullida y no s'alza. As columnas se fan listas y os carrils se quedan carrils; as fayenas mantienen a suya descripción, color, etiquetas, calendata de vencimiento, subfayenas como lista de comprebación, tiempo emplegau, estimación, personas y comentarios.",
@@ -941,19 +941,19 @@
"api-import-fetched": "Recullido. Revisa os miembros d'abaixo y importa.",
"api-import-failed": "A recullida ha fallau",
"api-import-nothing": "No i hai cosa que importar con esta cuenta.",
"import-board-instruction-pivotal": "In Pivotal Tracker, open the project's MORE tab and choose Export CSV (or select stories and choose Bulk Actions, CSV), then paste the CSV. Each story state becomes a list and stories become cards, with their type and labels, owners, requester, dates, comments, tasks and blockers; Estimate becomes Story points and iterations become sprints.",
"import-board-instruction-tasksorg": "In Tasks.org, open Settings, Backups and choose Export tasks, then paste the contents of that .json file. Lists become lists and tasks become cards, with their notes, tags, dates, comments and subtasks; completed tasks keep their completion date, and priority becomes a custom field.",
"import-board-instruction-monday": "In monday.com, open the board menu, choose More actions and Export board to Excel (with updates and subitems if you like), then choose that .xlsx file. The Status column becomes lists and groups become swimlanes; people, dates, timeline and tags keep their places, other columns become custom fields and updates become comments.",
"import-board-instruction-superproductivity": "In Super Productivity, open Settings, then Sync & Backup, choose Export Data and paste the contents of the sp-backup .json file. Each project becomes a swimlane and tasks become cards in To Do, In Progress, Backlog or Done, with their notes, tags, due date, time spent and sub-tasks; archived tasks become archived cards.",
"import-board-instruction-taiga": "In Taiga, open Admin > Project > Export and download the project dump (unpack a .json.gz file first), then paste its JSON. User story statuses become lists, user stories become cards with their tasks as subtasks, epics and issues get swimlanes of their own, and sprints, tags, comments and custom attributes come along. Attachments are not imported.",
"import-board-instruction-vikunja": "In Vikunja, open Settings, choose Data Export and download the .zip file it sends you, then choose that file, or paste the data.json inside it. Kanban buckets become lists and tasks become cards, with their labels, assignees, dates, description, task lists as checklists and comments; priority, percent done and done become custom fields. Attachments are not imported.",
"import-board-instruction-quire": "In Quire, open the project menu and choose Export CSV, then paste the CSV. Statuses become lists and tasks become cards, with their subtasks as child cards, assignees, tags as labels, start, due and completed dates, description and priority as a custom field. Comments and attachments are not in Quire's CSV.",
"import-board-instruction-wrike": "In Wrike, export a project or folder to Excel, or fill in Wrike's Excel import template, then choose that .xlsx file. Folder and project rows become swimlanes, Status becomes lists and tasks become cards, with their Key, parent task, assignees, start and end dates, dependencies and description; Priority, Duration and other columns become custom fields.",
"import-board-instruction-teamwork": "In Teamwork.com, fill in the Excel task import template (Tasklist, Task, Description, Assign to, Start date, Due date, Priority, Estimated time, Tags, Status) and save it as .xlsx, then choose that file. Task lists become lists and tasks become cards; a task starting with -, # or > becomes a subtask of the task above it (--, ## or >> one level deeper). Assignees, dates and tags keep their places; Priority, Estimated time and Complete become custom fields.",
"import-board-instruction-businessmap": "In Businessmap (formerly Kanbanize), open Advanced Search, choose Configure results to pick the columns, download the Excel file, then choose that .xlsx file. Columns are read by their English names (Title, Column, Lane, Owner, Deadline ...): if your account uses another language, rename the header row to English first. Column becomes lists and Lane swimlanes; owners, tags, color, dates, comments and parent links keep their places, and Priority, Size, Type and other columns become custom fields.",
"import-board-instruction-redmine": "In Redmine, open the project's Issues, choose Also available in: CSV at the bottom of the list, pick the columns (All columns, with Description) and export, then paste the CSV. Redmine names the columns in your language, so set your language to English in My account before exporting. Each status becomes a list and issues become cards, with the tracker as a label, assignee, author, dates, parent task and related issues; priority, category, target version, estimated time, % Done and custom fields become custom fields.",
"import-board-instruction-notion": "In Notion, open the database as a full page, choose ••• and Export, pick Markdown & CSV, and choose the .zip file it downloads, or one database CSV from it, or paste that CSV. A column named Status becomes the lists (without one, the first column that looks like a select does, and the import report says which); rows become cards with their title, tags as labels, people as members, dates, and the page text as the description; other columns become custom fields. Relations, images and attachments are not imported.",
"import-board-instruction-plane": "In Plane, open Workspace Settings, choose Exports, export the projects as JSON, CSV or Excel and download the .zip file, then choose that file, or one file from inside it, or paste the JSON or CSV. States become lists, projects become swimlanes, and issues become cards with their identifier, parent, assignees, labels, dates, links and comments; priority, estimate, cycle and module become custom fields. Plane's export has no descriptions or attachments, so they are not imported.",
"import-board-instruction-pivotal": "En Pivotal Tracker, ubre a pestanya MORE d'o prochecto y tría Export CSV (u selecciona historias y tría Bulk Actions, CSV), dimpués apega o CSV. Cada estau d'historia se torna una lista y as historias se tornan tarchetas, con o suyo tipo y etiquetas, propietarios, solicitant, calendatas, comentarios, tareas y bloqueos; Estimate se torna Story points y as iteracions se tornan sprints.",
"import-board-instruction-tasksorg": "En Tasks.org, ubre Settings, Backups y tría Export tasks, dimpués apega o conteniu d'ixe fichero .json. As listas se tornan listas y as tareas se tornan tarchetas, con as suyas notas, etiquetas, calendatas, comentarios y subtareas; as tareas completadas conservan a suya calendata de remate, y a prioridat se torna un campo personalizau.",
"import-board-instruction-monday": "En monday.com, ubre o menú d'o tablero, tría More actions y Export board to Excel (con updates y subitems si quiers), dimpués tría ixe fichero .xlsx. A columna Status se torna listas y os grupos se tornan carrils; personas, calendatas, timeline y etiquetas conservan o suyo puesto, as atras columnas se tornan campos personalizaus y os updates se tornan comentarios.",
"import-board-instruction-superproductivity": "En Super Productivity, ubre Settings, dimpués Sync & Backup, tría Export Data y apega o conteniu d'o fichero sp-backup .json. Cada prochecto se torna un carril y as tareas se tornan tarchetas en To Do, In Progress, Backlog u Done, con as suyas notas, etiquetas, calendata de vencimiento, tiempo emplegau y subtareas; as tareas archivadas se tornan tarchetas archivadas.",
"import-board-instruction-taiga": "En Taiga, ubre Admin > Project > Export y descarga o volcau d'o prochecto (descomprime primero un fichero .json.gz), dimpués apega o suyo JSON. Os estaus d'as historias d'usuario se tornan listas, as historias d'usuario se tornan tarchetas con as suyas tareas como subtareas, as epicas y incidencias tienen carrils propios, y os sprints, etiquetas, comentarios y atributos personalizaus tamién plegan. Os adchuntos no s'importan.",
"import-board-instruction-vikunja": "En Vikunja, ubre Settings, tría Data Export y descarga o fichero .zip que te ninvia, dimpués tría ixe fichero, u apega o data.json que i hai adintro. Os cubos Kanban se tornan listas y as tareas se tornan tarchetas, con as suyas etiquetas, asignaus, calendatas, descripción, listas de tareas como listas de comprebación y comentarios; a prioridat, o percentache feito y feito se tornan campos personalizaus. Os adchuntos no s'importan.",
"import-board-instruction-quire": "En Quire, ubre o menú d'o prochecto y tría Export CSV, dimpués apega o CSV. Os estaus se tornan listas y as tareas se tornan tarchetas, con as suyas subtareas como tarchetas fillas, asignaus, etiquetas, calendatas d'inicio, vencimiento y remate, descripción y prioridat como campo personalizau. Os comentarios y adchuntos no son en o CSV de Quire.",
"import-board-instruction-wrike": "En Wrike, exporta un prochecto u carpeta ta Excel, u replena a plantilla d'importación Excel de Wrike, dimpués tría ixe fichero .xlsx. As filas de carpeta y prochecto se tornan carrils, Status se torna listas y as tareas se tornan tarchetas, con a suya Key, tarea pai, asignaus, calendatas d'inicio y fin, dependencias y descripción; Priority, Duration y atras columnas se tornan campos personalizaus.",
"import-board-instruction-teamwork": "En Teamwork.com, replena a plantilla Excel d'importación de tareas (Tasklist, Task, Description, Assign to, Start date, Due date, Priority, Estimated time, Tags, Status) y alza-la como .xlsx, dimpués tría ixe fichero. As listas de tareas se tornan listas y as tareas se tornan tarchetas; una tarea que empecipia con -, # u > se torna una subtarea d'a tarea d'alto (--, ## u >> un nivel mas fondo). Asignaus, calendatas y etiquetas conservan o suyo puesto; Priority, Estimated time y Complete se tornan campos personalizaus.",
"import-board-instruction-businessmap": "En Businessmap (antes Kanbanize), ubre Advanced Search, tría Configure results pa eslechir as columnas, descarga o fichero Excel, dimpués tría ixe fichero .xlsx. As columnas se leyen por os suyos nombres en anglés (Title, Column, Lane, Owner, Deadline ...): si a tuya cuenta fa servir unatra luenga, renombra primero a fila de capitero en anglés. Column se torna listas y Lane carrils; propietarios, etiquetas, color, calendatas, comentarios y vinclos pai conservan o suyo puesto, y Priority, Size, Type y atras columnas se tornan campos personalizaus.",
"import-board-instruction-redmine": "En Redmine, ubre as Issues d'o prochecto, tría Also available in: CSV en o baixo d'a lista, eslei as columnas (All columns, con Description) y exporta, dimpués apega o CSV. Redmine nombra as columnas en a tuya luenga, asinas que meteba a tuya luenga en anglés en My account antes d'exportar. Cada estau se torna una lista y as incidencias se tornan tarchetas, con o tracker como etiqueta, asignau, autor, calendatas, tarea pai y incidencias relacionadas; prioridat, categoría, versión obchectivo, tiempo estimau, % Done y campos personalizaus se tornan campos personalizaus.",
"import-board-instruction-notion": "En Notion, ubre a base de datos como pachina completa, tría ••• y Export, eslei Markdown & CSV, y tría o fichero .zip que se descarga, u un CSV de base de datos d'adintro, u apega ixe CSV. Una columna clamada Status se torna as listas (si no i ye, la fa a primer columna que pareixe una selección, y o informe d'importación diz quala); as filas se tornan tarchetas con o suyo titol, etiquetas como etiquetas, personas como miembros, calendatas, y o texto d'a pachina como descripción; as atras columnas se tornan campos personalizaus. As relacions, imachens y adchuntos no s'importan.",
"import-board-instruction-plane": "En Plane, ubre Workspace Settings, tría Exports, exporta os prochectos como JSON, CSV u Excel y descarga o fichero .zip, dimpués tría ixe fichero, u un fichero d'adintro, u apega o JSON u o CSV. Os estaus se tornan listas, os prochectos se tornan carrils, y as incidencias se tornan tarchetas con o suyo identificador, pai, asignaus, etiquetas, calendatas, vinclos y comentarios; prioridat, estimación, cicle y modulo se tornan campos personalizaus. A exportación de Plane no tiene descripcions ni adchuntos, asinas que no s'importan.",
"import-board-instruction-trello": "En o tuyo tablero de Trello, ves ta 'Menú', dimpués 'Más', 'Imprimir y exportar', 'Exportar JSON', y copia o texto resultante.",
"import-board-instruction-csv": "Apega os tuyos valors separaus por comas (CSV)/valors separaus por tabuladors (TSV).",
"import-board-instruction-jira": "Apega o JSON d'a busca d'incidencias de l'API REST de Jira Cloud (GET /rest/api/3/search/jql), u un obchecto equivalent { \"issues\": [...] }. Os estaus de Jira se tornan listas y as incidencias se tornan tarchetas. Opcionalment, incluye una matriz \"automationRules\" ta importar reglas.",
@@ -962,33 +962,33 @@
"import-board-instruction-wekan": "En o tuyo tablero, ves ta 'Menú', dimpués 'Exportar tablero', y copia o texto d'o fichero descargau.",
"import-board-instruction-about-errors": "Si tiens errors en importar o tablero, a vegadas a importación funciona igualment y o tablero ye en a pachina de Todos os tableros.",
"import-without-mapping-members": "Importar sin asignar miembros (asignar mas tarde)",
"import-members-mode-heading": "People in the file:",
"import-members-mode-map": "Choose an existing user for each of them",
"import-members-mode-placeholder": "Keep them as placeholder users with their own names, to map to real users later",
"import-members-mode-me": "Make them all me",
"csv-mapping-title": "Which column is which?",
"csv-mapping-description": "Choose the column of the file that holds each card field. The choices were made from the column names; change any that are wrong, and choose (not in the file) for a field the file does not have.",
"csv-mapping-not-in-file": "(not in the file)",
"csv-mapping-column-number": "Column __number__",
"csv-mapping-list-missing": "The file has no column for the list. Choose one above, or type the name of the one list every card goes into.",
"csv-mapping-list-name": "List for every card",
"csv-mapping-custom-fields": "Other columns to import as text custom fields:",
"csv-mapping-boards": "Each sheet becomes its own board:",
"csv-mapping-skipped-sheets": "Sheets that are not imported:",
"csv-mapping-title-required": "Choose the column that holds the card titles.",
"csv-mapping-list-required": "Type the name of the list every card goes into, or choose the column that holds the list.",
"invalid-import-mapping": "The column mapping does not match the file.",
"import-many-boards": "Import many boards",
"import-many-boards-hint": "Choose several export files of this tool, or one .zip that holds them: each file becomes its own board, imported without member mapping. A .zip that is itself one export (Vikunja, Notion, Plane, or WeKan with attachments) is one board. When files are chosen here, Import imports them instead of the field above.",
"import-one-board-per-project": "One board per project",
"import-one-board-per-project-hint": "Each swimlane the import would make (a project, folder or list of the tool) becomes its own board. Parent cards and dependencies between those boards are listed in the import report.",
"import-many-progress": "Importing __done__ of __total__: __name__",
"import-many-results-heading": "Imported files",
"import-many-imported": "imported",
"export-all-boards": "Export all boards",
"export-selected-boards": "Export selected boards",
"export-all-boards-hint": "Every board you can export, in one download: Excel as one workbook with a sheet per board named after it, every other format as a .zip with a file per board.",
"exportAllBoardsPopup-title": "Export all boards",
"import-members-mode-heading": "Personas en o fichero:",
"import-members-mode-map": "Tría un usuario existent pa cadaguna d'ellas",
"import-members-mode-placeholder": "Conservar-las como usuarios provisionals con os suyos nombres, pa asignar-las a usuarios reals mas tardi",
"import-members-mode-me": "Fer que totz sigan yo",
"csv-mapping-title": "Quala columna ye quala?",
"csv-mapping-description": "Tría a columna d'o fichero que tiene cada campo d'a tarcheta. As opcions s'han feito a partir d'os nombres d'as columnas; cambia as que sigan erronias, y tría (no ye en o fichero) pa un campo que o fichero no tiene.",
"csv-mapping-not-in-file": "(no ye en o fichero)",
"csv-mapping-column-number": "Columna __number__",
"csv-mapping-list-missing": "O fichero no tiene columna pa la lista. Tría-ne una d'alto, u escribe o nombre d'a sola lista an que van todas as tarchetas.",
"csv-mapping-list-name": "Lista pa todas as tarchetas",
"csv-mapping-custom-fields": "Atras columnas a importar como campos personalizaus de texto:",
"csv-mapping-boards": "Cada fuella se torna un tablero propio:",
"csv-mapping-skipped-sheets": "Fuellas que no s'importan:",
"csv-mapping-title-required": "Tría a columna que tiene os titols d'as tarchetas.",
"csv-mapping-list-required": "Escribe o nombre d'a lista an que van todas as tarchetas, u tría a columna que tiene a lista.",
"invalid-import-mapping": "L'asignación de columnas no coincide con o fichero.",
"import-many-boards": "Importar muitos tableros",
"import-many-boards-hint": "Tría quantos fichers d'exportación d'esta ferramienta, u un .zip que los contienga: cada fichero se torna un tablero propio, importau sin asignación de miembros. Un .zip que ye ell mesmo una exportación (Vikunja, Notion, Plane, u WeKan con adchuntos) ye un solo tablero. Quan se trían fichers aquí, Importar importa ixos en cuenta d'o campo d'alto.",
"import-one-board-per-project": "Un tablero por prochecto",
"import-one-board-per-project-hint": "Cada carril que fería a importación (un prochecto, carpeta u lista d'a ferramienta) se torna un tablero propio. As tarchetas pai y as dependencias entre ixos tableros s'amuestran en l'informe d'importación.",
"import-many-progress": "Importando __done__ de __total__: __name__",
"import-many-results-heading": "Fichers importaus",
"import-many-imported": "importau",
"export-all-boards": "Exportar totz os tableros",
"export-selected-boards": "Exportar os tableros seleccionaus",
"export-all-boards-hint": "Totz os tableros que puetz exportar, en una sola descarga: Excel como un libro con una fuella por tablero con o suyo nombre, qualsequier atro formato como un .zip con un fichero por tablero.",
"exportAllBoardsPopup-title": "Exportar totz os tableros",
"import-json-placeholder": "Apega os tuyos datos JSON validos aquí",
"import-csv-placeholder": "Apega os tuyos datos CSV/TSV validos aquí",
"import-json-file": "O selecciona un fichero JSON de Trello",
@@ -1026,17 +1026,17 @@
"trello-import-results": "Resultaus d’a importación",
"trello-api-credentials-required": "Introduz tanto a clau API como o token de Trello.",
"trello-api-credentials-saved": "A clau API y o token de Trello son alzaus. Puetz listar espacios de treballo sin tornar a introducir-los.",
"exportToTrelloPopup-title": "Export to Trello",
"trello-export-desc": "Create a copy of this board in Trello through Trello's API, with your Trello API key and token. Swimlanes become labels named \"Swimlane: …\", which WeKan's Trello import turns back into swimlanes.",
"trello-export-include-attachments": "Include attachments",
"trello-export-start": "Export to Trello",
"trello-export-open-board": "Open the board in Trello",
"trello-export-warnings": "Not everything could be exported",
"sandstorm-trello-access-needed": "A Sandstorm grain has no Internet access of its own. Allow this grain to reach Trello first.",
"sandstorm-allow-trello-access": "Allow access to Trello",
"sandstorm-trello-access-granted": "This grain may reach Trello.",
"sandstorm-forget-access": "Forget",
"sandstorm-trello-save-label": "Trello, for importing and exporting boards",
"exportToTrelloPopup-title": "Exportar ta Trello",
"trello-export-desc": "Creya una copia d'este tablero en Trello a traviés d'a API de Trello, con a tuya clau API y token de Trello. Os carrils se tornan etiquetas clamadas \"Swimlane: …\", que la importación de Trello de WeKan torna a convertir en carrils.",
"trello-export-include-attachments": "Incluir adchuntos",
"trello-export-start": "Exportar ta Trello",
"trello-export-open-board": "Ubrir o tablero en Trello",
"trello-export-warnings": "No s'ha puesto exportar tot",
"sandstorm-trello-access-needed": "Un grain de Sandstorm no tiene acceso propio a Internet. Permite primero que este grain plegue ta Trello.",
"sandstorm-allow-trello-access": "Permitir l'acceso ta Trello",
"sandstorm-trello-access-granted": "Este grain puet plegar ta Trello.",
"sandstorm-forget-access": "Ixublidar",
"sandstorm-trello-save-label": "Trello, pa importar y exportar tableros",
"trello-select-boards": "Selecciona t'o menos un tablero ta importar.",
"select-all": "Seleccionar todo",
"select-none": "No seleccionar ninguno",
@@ -1311,15 +1311,16 @@
"custom-login-logo-link-url": "URL de enlace d'o logo personalizado de inicio de sesión",
"custom-help-link-url": "URL d’o vinclo d’aduya personalizau",
"text-below-custom-login-logo": "Texto debajo d'o logo personalizado de inicio de sesión",
"login-background-color": "Color de fondo d'a pachina d'inicio de sesión",
"automatic-linked-url-schemes": "Esquemas d’URL personalizaus que han de poder clicar-se automaticament. Un esquema d’URL por linia.",
"automatic-linked-url-schemes-hint": "Vuedo por defecto, asinas que nomás se puede fer clic en os vinclos web y de correu. Cada esquema d'a lista, por eixemplo thunderlink u onenote, ubre l'aplicación rechistrada ta ell. javascript, data, vbscript y esquemas pareixius nunca no se tornan vinclos.",
"external-link-pattern": "Vinclo automatico ta un rastrexador de problemas externo",
"external-link-pattern-description": "Vincla automaticament os tokens estilo \"#1234\" trobatos en o texto d'a targeta ta un rastrexador de problemas externo. Dixa un campo baleiro ta desactivar-lo.",
"external-link-pattern-prefix": "Prefixo d'o token",
"external-link-pattern-url": "Plantilla d'URL (fe servir {number})",
"external-link-rules": "More link rules",
"external-link-rules-description": "One rule per line: the text to find, \" = \", and the address. Use {number} for digits and {identifier} for a word, in both, for example [{identifier}:{number}] = https://tracker.example.com/{identifier}/{number}.",
"external-link-identifier-aliases": "Abbreviations for {identifier}, for example TK=Task, IN=Incident",
"external-link-rules": "Mas reglas de vinclos",
"external-link-rules-description": "Una regla por linia: o texto a mirar, \" = \", y l'adreza. Fe servir {number} pa os numeros y {identifier} pa una parola, en as dos, por eixemplo [{identifier}:{number}] = https://tracker.example.com/{identifier}/{number}.",
"external-link-identifier-aliases": "Abreviaturas pa {identifier}, por eixemplo TK=Task, IN=Incident",
"username": "Nombre d'usuario",
"import-usernames": "Importar Usuarios",
"view-it": "Verla",
@@ -1344,13 +1345,13 @@
"attachment-transfer-limits-title": "Límites de tamaño de fichero de adjuntos y API",
"attachment-limits": "Límites",
"attachment-retention": "Retención",
"attachment-retention-days": "Delete attachments after (days)",
"attachment-retention-days-help": "Like Delete on the card: the card history can restore them. Empty or 0: never.",
"attachment-retention-mode": "Count the days from",
"attachment-retention-purge": "Remove deleted attachments for good after (days)",
"attachment-retention-purge-help": "After this they can no longer be restored. Empty or 0: never.",
"attachment-retention-dry-run": "Dry run: only count, in the server log, change nothing",
"attachment-retention-max-per-run": "At most this many attachments per night",
"attachment-retention-days": "Borrar os adchuntos dimpués de (días)",
"attachment-retention-days-help": "Como Borrar en a tarcheta: l'historial d'a tarcheta los puet restaurar. Vuedo u 0: nunca.",
"attachment-retention-mode": "Contar os días dende",
"attachment-retention-purge": "Eliminar pa cutio os adchuntos borraus dimpués de (días)",
"attachment-retention-purge-help": "Dimpués d'isto ya no se pueden restaurar. Vuedo u 0: nunca.",
"attachment-retention-dry-run": "Proba: nomás contar, en o rechistro d'o servidor, sin cambiar cosa",
"attachment-retention-max-per-run": "Como muito tantos adchuntos por nueit",
"attachment-transfer-limits-description": "Configura os límites máximos de tamaño de fichero ta subidas y descargas. El almacenamiento en búfer de as transferencias de la API tiene topes de seguridad independientes d'o lado d'o servidor.",
"attachment-transfer-limits-saved": "Limites de transferencia d’adchuntos alzaus.",
"attachment-transfer-limits-save-failed": "Error t'o alzar os límites de transferencia de adjuntos",
@@ -1400,51 +1401,51 @@
"error-notAuthorized": "No estás autorizado a veyer ista página.",
"webhook-title": "Nombre d'o Webhook",
"webhook-token": "Token (opcional ta la autenticación)",
"webhook-payload-heading": "Webhook data",
"webhook-payload-description": "What outgoing webhooks (Rocket.Chat, Slack, ...) send. Default keeps the inherited setting; with nothing set, webhooks send what they always have.",
"webhook-payload-text": "Send the message text",
"webhook-payload-fields": "Send these fields",
"webhook-payload-field-standard": "Standard fields (WEBHOOKS_ATTRIBUTES)",
"webhook-payload-field-ids": "IDs",
"webhook-payload-field-names": "Names and titles",
"webhook-payload-field-links": "Links to the card and board",
"webhook-payload-field-actor": "Who did it",
"webhook-payload-field-people": "The member or assignee it is about",
"webhook-payload-field-details": "Details: comment, description, values and dates",
"webhook-hide-identity": "Leave my name out of outgoing webhooks",
"webhook-someone": "A member",
"webhook-payload-heading": "Datos d'o webhook",
"webhook-payload-description": "Que ninvian os webhooks salients (Rocket.Chat, Slack, ...). Por defecto se conserva a configuración heredada; si no i hai cosa configurada, os webhooks ninvian o que siempre han ninviau.",
"webhook-payload-text": "Ninviar o texto d'o mensache",
"webhook-payload-fields": "Ninviar estes campos",
"webhook-payload-field-standard": "Campos estandar (WEBHOOKS_ATTRIBUTES)",
"webhook-payload-field-ids": "Identificadors",
"webhook-payload-field-names": "Nombres y titols",
"webhook-payload-field-links": "Vinclos ta la tarcheta y o tablero",
"webhook-payload-field-actor": "Qui lo fació",
"webhook-payload-field-people": "O miembro u asignau a qui se refiere",
"webhook-payload-field-details": "Detalles: comentario, descripción, valors y calendatas",
"webhook-hide-identity": "No meter o mío nombre en os webhooks salients",
"webhook-someone": "Un miembro",
"notification-delivery-layout": "Diseño",
"notification-delivery-layout-classic": "One sentence (classic)",
"notification-delivery-layout-clear": "Clearly arranged: board, who, card in bold",
"notification-delivery-parts": "Show",
"notification-delivery-part-board": "Board name and link",
"notification-delivery-part-actor": "Who did it",
"notification-delivery-part-card": "Card title and link",
"notification-delivery-layout-classic": "Una frase (clasico)",
"notification-delivery-layout-clear": "Ordenau con claridat: tablero, qui, tarcheta en negreta",
"notification-delivery-parts": "Amostrar",
"notification-delivery-part-board": "Nombre y vinclo d'o tablero",
"notification-delivery-part-actor": "Qui lo fació",
"notification-delivery-part-card": "Titol y vinclo d'a tarcheta",
"notification-delivery-part-list": "Lista",
"notification-delivery-part-swimlane": "Carril",
"notification-delivery-part-details": "Comment or changed value",
"notification-delivery-part-dates": "Dates",
"notification-delivery-part-link": "Link on its own line",
"notification-delivery-grouping": "Grouping",
"notification-delivery-grouping-all": "Everything due in one message",
"notification-delivery-grouping-board": "One per board",
"notification-delivery-grouping-card": "One per card",
"notification-delivery-grouping-none": "Each on its own",
"notification-delivery-schedule": "When to send",
"notification-delivery-schedule-immediate": "Immediately",
"notification-delivery-schedule-interval": "Every few minutes or hours",
"notification-delivery-schedule-daily": "Once a day",
"notification-delivery-interval": "Every",
"notification-delivery-minutes": "__count__ minutes",
"notification-delivery-part-details": "Comentario u valor cambiada",
"notification-delivery-part-dates": "Calendatas",
"notification-delivery-part-link": "Vinclo en a suya propia linia",
"notification-delivery-grouping": "Agrupación",
"notification-delivery-grouping-all": "Tot o pendient en un solo mensache",
"notification-delivery-grouping-board": "Un por tablero",
"notification-delivery-grouping-card": "Un por tarcheta",
"notification-delivery-grouping-none": "Cadaguno por separau",
"notification-delivery-schedule": "Quan ninviar",
"notification-delivery-schedule-immediate": "Immediatament",
"notification-delivery-schedule-interval": "Cada quantos minutos u horas",
"notification-delivery-schedule-daily": "Una vegada a o día",
"notification-delivery-interval": "Cada",
"notification-delivery-minutes": "__count__ minutos",
"notification-delivery-daily-time": "Hora",
"notification-delivery-quiet": "Quiet hours (wait until they end)",
"notification-delivery-quiet": "Horas de silencio (aguardar dica que rematen)",
"notification-delivery-quiet-from": "Dende",
"notification-delivery-quiet-to": "Dica",
"notification-delivery-quiet-clear": "No quiet hours",
"notification-delivery-timezone": "Time zone:",
"notification-delivery-invalid": "The setting could not be saved.",
"notification-group-card": "__count__ notifications on card __card__",
"notification-group-board": "__count__ notifications on board __board__",
"notification-delivery-quiet-clear": "Sin horas de silencio",
"notification-delivery-timezone": "Zona horaria:",
"notification-delivery-invalid": "No s'ha puesto alzar a configuración.",
"notification-group-card": "__count__ notificacions en a tarcheta __card__",
"notification-group-board": "__count__ notificacions en o tablero __board__",
"outgoing-webhooks": "Webhooks salientes",
"bidirectional-webhooks": "Webhooks de doble sentiu",
"outgoingWebhooksPopup-title": "Webhooks salientes",
@@ -1499,7 +1500,7 @@
"showLabel-field-on-card": "Amostrar a etiqueta d’o campo en a minitarcheta",
"showSum-field-on-list": "Mostrar la suma de os campos en la parte superior de la lista",
"admin-only-field": "Visible nomás ta ra almins d'o tablero",
"read-only-field": "Read-only: every member sees the value, only board admins change it",
"read-only-field": "Nomás de lectura: totz os miembros veyen a valor, nomás os administradors d'o tablero la cambian",
"yes": "Sí",
"no": "No",
"accounts": "Cuentas",
@@ -1691,11 +1692,11 @@
"r-format-auto": "Detección automática",
"r-import-workflow-note": "Importa un flujo de trabajo visual exportado dende n8n o Node-RED. Os nodos de disparador/acción reconocidos se mapean a reglas de WeKan; os nodos no mapeados se informan.",
"r-import-unmapped": "No s’ha puesto establir a correspondencia de __count__ linia(s)",
"r-wrike-workflow": "Wrike workflow",
"r-wrike-workflow-note": "A Wrike workflow is a list of statuses, each in the Active, Completed, Deferred or Cancelled group. Export writes this board's lists as one, named after the board, in the JSON Wrike's API returns for workflows: create that workflow in Wrike before importing the board's Wrike Excel export, so its statuses are applied. Import reads that JSON pasted above, from WeKan or from Wrike's GET /workflows, into the chosen board: a status without a list becomes a list, and a rule marks a card complete when it is moved into a Completed or Cancelled status and incomplete in an Active or Deferred one. Wrike's own automation rules cannot be exported from Wrike.",
"r-export-wrike-workflow": "Export Wrike workflow",
"r-import-wrike-workflow": "Import Wrike workflow",
"r-import-wrike-workflow-done": "Wrike workflow \"__name__\": __lists__ list(s) and __rules__ rule(s) added, __skipped__ part(s) not read",
"r-wrike-workflow": "Fluxo de treballo de Wrike",
"r-wrike-workflow-note": "Un fluxo de treballo de Wrike ye una lista d'estaus, cadaguno en o grupo Active, Completed, Deferred u Cancelled. A exportación escribe as listas d'este tablero como un solo fluxo, con o nombre d'o tablero, en o JSON que torna a API de Wrike pa os fluxos de treballo: creya ixe fluxo en Wrike antes d'importar a exportación Excel de Wrike d'o tablero, ta que s'apliquen os suyos estaus. A importación leye ixe JSON apegau alto, de WeKan u d'o GET /workflows de Wrike, en o tablero eslechiu: un estau sin lista se torna una lista, y una regla marca una tarcheta como completada quan se mueve ta un estau Completed u Cancelled y como no completada en un estau Active u Deferred. As reglas d'automatización propias de Wrike no se pueden exportar dende Wrike.",
"r-export-wrike-workflow": "Exportar o fluxo de treballo de Wrike",
"r-import-wrike-workflow": "Importar o fluxo de treballo de Wrike",
"r-import-wrike-workflow-done": "Fluxo de treballo de Wrike \"__name__\": __lists__ lista(s) y __rules__ regla(s) adhibidas, __skipped__ parte(s) no leyidas",
"r-set-scheduled-triggers": "Disparadores programados",
"r-set-button-triggers": "Botones",
"r-when-scheduled": "Según un horario",
@@ -1751,8 +1752,8 @@
"set-filter": "Filtrar",
"r-moved-to": "Movido a",
"r-moved-from": "Movido dende",
"r-moved-forward": "moved forward to a later list",
"r-moved-back": "moved back to an earlier list",
"r-moved-forward": "moviu enta debant ta una lista posterior",
"r-moved-back": "moviu enta zaga ta una lista anterior",
"r-archived": "Se archivó",
"r-unarchived": "Restaurado d'o fichero",
"r-commented": "se comentó",
@@ -1800,8 +1801,8 @@
"r-add-actinguser-member": "Adibir l'usuario que ha activau ista regla como miembro",
"r-remove-all": "Borrar totz os miembros de la tarcheta",
"r-assignee": "Asignar",
"r-add-actinguser-assignee": "Add the user who triggered this rule as an assignee",
"r-remove-all-assignees": "Remove all assignees from the card",
"r-add-actinguser-assignee": "Adhibir l'usuario que activó esta regla como asignau",
"r-remove-all-assignees": "Sacar totz os asignaus d'a tarcheta",
"r-remove-all-labels": "Borrar totz os etiquetas de la tarcheta",
"r-set-color": "Cambiar el color a",
"r-checklist": "lista de verificación",
@@ -1875,10 +1876,10 @@
"ldap-test-connection": "Prebar a connexión LDAP",
"ldap-test-connection-success": "Connexión con exito",
"ldap-test-connection-error": "Ha fallau a connexión: %s",
"ldap-sync-now": "Sync LDAP users now",
"ldap-sync-now-done": "LDAP sync finished",
"ldap-sync-now-error": "LDAP sync failed: %s",
"ldap-sync-now-nothing": "Nothing to sync: turn on LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS or LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED first.",
"ldap-sync-now": "Sincronizar agora os usuarios LDAP",
"ldap-sync-now-done": "Sincronización LDAP rematada",
"ldap-sync-now-error": "A sincronización LDAP ha fallau: %s",
"ldap-sync-now-nothing": "No i hai cosa que sincronizar: activa primero LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS u LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED.",
"oauth-providers-title": "Furnidors d’inicio de sesión OAuth",
"oauth-providers-hint": "Cada furnidor se puede activar aquí u con a suya variable d’entorno OAUTH_*_ENABLED; una valor definida aquí anula a variable d’entorno. O secreto se alza en o servidor y nunca no s’amuestra.",
"oauth-provider-google": "Google",
@@ -1893,7 +1894,7 @@
"oauth-provider-secret-set": "Bi ha un secreto definiu",
"oauth-providers-login-style": "Estilo d’inicio de sesión",
"oauth-providers-merge-existing-users": "Fusionar con una cuenta existent que tienga o mesmo correu electronico",
"oauth-providers-allowed-email-domains": "Only these email domains may sign in with these providers (comma separated; empty allows all)",
"oauth-providers-allowed-email-domains": "Nomás estes dominios de correu pueden dentrar con estes proveyedors (separaus por comas; vuedo permite totz)",
"oauth-account-conflict": "Ya existe una cuenta con iste correu electronico y no se creyó con iste metodo d’inicio de sesión",
"sign-in-with": "Iniciar sesión con %s",
"login-origin-mismatch": "No se puede completar l’inicio de sesión por meyo d’unatro servicio en esta adreza. WeKan ye configurau ta __expected__, pero esta pachina s’ha ubierto en __actual__. Ubre WeKan en __expected__, u pide a l’administrador que configure ROOT_URL como __actual__.",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Quan o titol d'una tarcheta nueva empecipia con [Texto] seguiu de mas texto, Texto se convierte en una etiqueta d'a tarcheta (se crea si o tablero no en tiene garra con ixe nombre) y se quita d'o titol. Desactivau: o titol se mantiene tal como ye.",
"keep-add-card-form-open": "Mantener ubierto o formulario d'adhibir tarcheta dimpués d'adhibir una tarcheta",
"keep-add-card-form-open-description": "Activau: dimpués d'adhibir una tarcheta, o formulario se mantién ubierto con un titol vuedo pa la siguient tarcheta. Desactivau: o formulario se zarra en cuanto s'adhibe la tarcheta.",
"show-focus-outline": "Amostrar o contorno de foco",
"checklist-ding-sound": "Sonar quan se completa un elemento d'a lista de verificación",
"checklist-ding-sound-description": "Sonar un tono curto quan marcas un elemento d'a lista de verificación. Desactivau por defecto.",
"assignee": "Asignau",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Conexión fallida",
"cloud-settings-saved": "Configuración d’almagazenamiento alzada",
"cloud-settings-save-failed": "Error t'o alzar la configuración de almacenamiento",
"cloud": "Boira",
"cloud-settings-desc": "Con WeKan en Weddy, o Cloud Panel sirve muitos dominios de WeKan dende un solo servidor, cadaguno con as suyas datos. En o dominio d'o Cloud Panel, o SuperAdmin chestiona os dominios y CloudFlare.",
"cloud-panel-domain": "Dominio d'o Cloud Panel",
"cloud-superadmin-username": "Nombre d'usuario d'o SuperAdmin",
"cloud-saved": "Alzau.",
"cloud-panel-not-allowed": "O Cloud Panel ye nomás pa o SuperAdmin, en o dominio d'o Cloud Panel.",
"cloud-customer-name": "Nombre d'o client",
"cloud-customer-email": "Adreza de correu d'o client",
"cloud-billing-email": "Adreza de correu de facturación",
"cloud-start-date": "Calendata d'inicio",
"cloud-end-date": "Calendata de fin",
"cloudflare-api-key": "Clau API de CloudFlare",
"cloudflare-api-key-desc": "Con una clau API de CloudFlare, un dominio que tienga la suya zona en a tuya cuenta de CloudFlare obtién un rechistro DNS con proxy y un certificau CloudFlare Origin. Os atros dominios obtienen un certificau Let's Encrypt. A clau se alza en o servidor y no se amuestra nunca mas.",
"cloudflare-api-key-set": "S'ha alzau una clau API de CloudFlare.",
"cloudflare-api-key-not-set": "No s'ha alzau garra clau API de CloudFlare: cada dominio obtién un certificau Let's Encrypt.",
"move-storage-azure": "Blob d'Azure",
"move-storage-gcs": "Almagazenamiento de Google Cloud",
"attachment-move-storage-azure": "Mover l’adchunto a Azure Blob Storage",
@@ -3274,7 +3291,7 @@
"import-report-open-board": "Ubrir o tablero",
"import-parts-instruction": "Solo se importan as partes marcadas. La misma selección se usa t'o exportar.",
"import-wekan-file": "Importar dende un fichero de exportación .json o .zip:",
"import-file-label": "Choose the export file, or paste its text below:",
"import-file-label": "Tría o fichero d'exportación, u apega o suyo texto debaixo:",
"sum-of-number-fields": "Suma d'os campos numericos personalizatos que amuestran a la parte alta d'a lista",
"date-range-of-fields": "Rango de calendata d'os campos personalizaus de calendata marcaus ta amostrar en a parte alta d'a lista",
"more-options": "Mas opcions",
@@ -3301,9 +3318,9 @@
"wip-limit-group-select-swimlane": "Triar un carril",
"wip-limit-group-apply-swimlane": "Aplicar Carril",
"subtask-inherit-parent-labels": "Heredar as etiquetas d'a tarcheta pai",
"subtask-mark-done": "Mark subtask as done",
"subtask-mark-not-done": "Mark subtask as not done",
"subtask-done-no-permission": "You cannot change whether this subtask is done",
"subtask-mark-done": "Marcar a subtarea como feita",
"subtask-mark-not-done": "Marcar a subtarea como no feita",
"subtask-done-no-permission": "No puetz cambiar si esta subtarea ye feita",
"chart-forecast-none-remaining": "No queda cosa ta prochectar - toda tarcheta ye feita.",
"chart-forecast-no-velocity": "__remaining__ tarchetas encara ubiertas; no i ha finalizacions recients ta prochectar una calendata.",
"chart-forecast-projected": "A lo ritmo recient de __average__ tarchetas/semana, as __remaining__ tarchetas encara ubiertas habrían d'estar feitas enta __date__.",
@@ -4378,52 +4395,52 @@
"rule-email-legacy-attempt-exists": "Iste correu ya s'ha ninviau, intentau ninviar u descartau.",
"rule-email-legacy-not-legacy": "Iste correu ya s'ha tornau a vincular.",
"rule-email-legacy-failed": "No s'ha puesto completar ista acción. Esviella y torna a intentar-lo.",
"stuck-sync-operation-heading": "List Sync operations that cannot be replayed",
"stuck-sync-operation-description": "Each of these saved List Sync operations stopped part way and can no longer be finished, because its list was removed, recreated or reconfigured, or the user who started it lost write access. While one is pending, that list cannot Sync. Discard it to let the list Sync again: changes already applied stay, the remaining saved changes are never written, and the next Sync compares the list with its source again.",
"stuck-sync-operation-list": "Board / list",
"stuck-sync-operation-heading": "Operacions de sincronización de lista que no se pueden tornar a executar",
"stuck-sync-operation-description": "Cadaguna d'estas operacions alzadas de sincronización de lista s'aturó a metat y ya no se puet rematar, porque a suya lista s'eliminó, se tornó a creyar u se reconfiguró, u l'usuario que la empecipió perdió l'acceso d'escritura. Mientres una ye pendient, ixa lista no se puet sincronizar. Descarta-la pa que a lista se pueda sincronizar de nuevo: os cambios ya aplicaus se conservan, os cambios alzaus que quedan nunca s'escriben, y a siguient sincronización compara de nuevo a lista con o suyo orichen.",
"stuck-sync-operation-list": "Tablero / lista",
"stuck-sync-operation-progress": "Progreso",
"stuck-sync-operation-reason": "Motivo",
"stuck-sync-operation-applied": "__applied__ of __total__ changes applied",
"stuck-sync-operation-reason-scope-changed": "The list was removed, recreated or its Sync settings changed",
"stuck-sync-operation-reason-access-denied": "The user who started it no longer has write access to the whole list",
"stuck-sync-operation-reason-trigger-unknown": "Saved by an earlier WeKan version without what started it",
"stuck-sync-operation-reason-intent-missing": "The record of who started it is missing",
"stuck-sync-operation-reason-unknown": "Could not be checked now",
"stuck-sync-operation-replayable-now": "It can be replayed again now and will finish by itself, so it cannot be discarded.",
"stuck-sync-operation-applied": "__applied__ de __total__ cambios aplicaus",
"stuck-sync-operation-reason-scope-changed": "A lista s'eliminó, se tornó a creyar u cambioron os suyos achustes de sincronización",
"stuck-sync-operation-reason-access-denied": "L'usuario que la empecipió ya no tiene acceso d'escritura a toda a lista",
"stuck-sync-operation-reason-trigger-unknown": "Alzada por una versión anterior de WeKan sin o que la empecipió",
"stuck-sync-operation-reason-intent-missing": "Falta o rechistro de qui la empecipió",
"stuck-sync-operation-reason-unknown": "No s'ha puesto comprebar agora",
"stuck-sync-operation-replayable-now": "Agora se puet tornar a executar y rematará ella sola, asinas que no se puet descartar.",
"stuck-sync-operation-discard": "Descartarla",
"stuck-sync-operation-discard-confirm": "Discard this saved List Sync operation? Changes it already applied stay; the rest are never written. The list can then Sync again.",
"stuck-sync-operation-discard-confirm": "Descartar esta operación alzada de sincronización de lista? Os cambios que ya aplicó se conservan; a resta nunca s'escribe. Dimpués a lista se puet sincronizar de nuevo.",
"stuck-sync-operation-refresh": "Actualizar",
"stuck-sync-operation-empty": "No List Sync operations are waiting for review.",
"stuck-sync-operation-truncated": "Only the oldest 50 are shown. Discard some and refresh to see more.",
"stuck-sync-operation-unavailable": "Could not load the List Sync operations. Refresh to try again.",
"stuck-sync-operation-missing": "This operation is no longer pending.",
"stuck-sync-operation-not-stuck": "This operation has not been found stuck, so it cannot be discarded.",
"stuck-sync-operation-replayable": "This operation can be replayed again now and will finish by itself, so it was not discarded.",
"stuck-sync-operation-busy": "This list is syncing right now. Try again in a moment.",
"stuck-sync-operation-empty": "No i hai operacions de sincronización de lista aguardando revisión.",
"stuck-sync-operation-truncated": "Nomás s'amuestran as 50 mas antigas. Descarta-ne bellas y refresca pa veyer-ne mas.",
"stuck-sync-operation-unavailable": "No s'han puesto cargar as operacions de sincronización de lista. Refresca pa tornar-lo a intentar.",
"stuck-sync-operation-missing": "Esta operación ya no ye pendient.",
"stuck-sync-operation-not-stuck": "Esta operación no s'ha trobau bloqueyada, asinas que no se puet descartar.",
"stuck-sync-operation-replayable": "Esta operación se puet tornar a executar agora y rematará ella sola, asinas que no s'ha descartau.",
"stuck-sync-operation-busy": "Esta lista se ye sincronizando agora mesmo. Torna-lo a intentar en un momento.",
"stuck-sync-operation-failed": "No s'ha puesto completar ista acción. Esviella y torna a intentar-lo.",
"interrupted-import-heading": "Board imports that stopped before finishing",
"interrupted-import-description": "Each of these board imports stopped part way - the server restarted, or the import failed - and left a partial board. An import cannot be continued, because its source file is not kept. Keep the board as it is, or discard it and import the file again. Discarding removes the board this import created and everything on it, including anything added to it since.",
"interrupted-import-heading": "Importacions de tableros que s'aturoron antes de rematar",
"interrupted-import-description": "Cadaguna d'estas importacions de tableros s'aturó a metat - o servidor se reinició, u a importación falló - y deixó un tablero parcial. Una importación no se puet continar, porque o suyo fichero d'orichen no se conserva. Conserva o tablero como ye, u descarta-lo y importa de nuevo o fichero. Descartar elimina o tablero que creyó esta importación y tot o que i hai, incluyiu tot o que s'i haiga adhibiu dende alavez.",
"interrupted-import-board": "Tablero",
"interrupted-import-progress": "Where it stopped",
"interrupted-import-created": "Created so far",
"interrupted-import-source": "Imported from __source__",
"interrupted-import-state-stopped": "Stopped without finishing, at __stage__",
"interrupted-import-state-failed": "Failed with __error__, at __stage__",
"interrupted-import-state-discarding": "Being discarded; discard again to finish",
"interrupted-import-progress": "An que s'aturó",
"interrupted-import-created": "Creyau dica agora",
"interrupted-import-source": "Importau dende __source__",
"interrupted-import-state-stopped": "Aturau sin rematar, en __stage__",
"interrupted-import-state-failed": "Fallau con __error__, en __stage__",
"interrupted-import-state-discarding": "Descartando-se; descarta de nuevo pa rematar",
"interrupted-import-scrum": "A suya fase Scrum tiene o suyo punto de control (__state__, paso __next__ de __total__). Si conservas o tablero, remata u descarta esta fase dende a recuperación d'importación Scrum d'o tablero.",
"interrupted-import-counts": "__swimlanes__ swimlanes, __lists__ lists, __cards__ cards, __checklists__ checklists, __comments__ comments, __attachments__ attachments",
"interrupted-import-no-board": "No board was created",
"interrupted-import-keep": "Keep",
"interrupted-import-counts": "__swimlanes__ carrils, __lists__ listas, __cards__ tarchetas, __checklists__ listas de comprebación, __comments__ comentarios, __attachments__ adchuntos",
"interrupted-import-no-board": "No s'ha creyau garra tablero",
"interrupted-import-keep": "Conservar",
"interrupted-import-discard": "Descartarla",
"interrupted-import-keep-confirm": "Keep this partial board as it is? Nothing is removed, and the import is no longer listed here.",
"interrupted-import-discard-confirm": "Discard this import? The board it created and everything on it are removed permanently.",
"interrupted-import-keep-confirm": "Conservar este tablero parcial como ye? No s'elimina cosa, y a importación ya no s'amostrará aquí.",
"interrupted-import-discard-confirm": "Descartar esta importación? O tablero que creyó y tot o que i hai s'eliminan pa cutio.",
"interrupted-import-refresh": "Actualizar",
"interrupted-import-empty": "No interrupted board imports are waiting for review.",
"interrupted-import-truncated": "Only the oldest 50 are shown. Keep or discard some and refresh to see more.",
"interrupted-import-unavailable": "Could not load the interrupted board imports. Refresh to try again.",
"interrupted-import-missing": "This import is no longer recorded.",
"interrupted-import-not-interrupted": "This import is not waiting for a decision any more.",
"interrupted-import-foreign-board": "The board with this id was not created by this import, so it was not touched.",
"interrupted-import-empty": "No i hai importacions de tableros interrumpidas aguardando revisión.",
"interrupted-import-truncated": "Nomás s'amuestran as 50 mas antigas. Conserva u descarta bellas y refresca pa veyer-ne mas.",
"interrupted-import-unavailable": "No s'han puesto cargar as importacions de tableros interrumpidas. Refresca pa tornar-lo a intentar.",
"interrupted-import-missing": "Esta importación ya no ye rechistrada.",
"interrupted-import-not-interrupted": "Esta importación ya no aguarda garra decisión.",
"interrupted-import-foreign-board": "O tablero con este id no lo creyó esta importación, asinas que no s'ha tocau.",
"interrupted-import-scrum-busy": "A suya fase Scrum encara s'está escribindo u recuperando. Torna a intentar-lo en uns pocos minutos.",
"interrupted-import-failed": "No s'ha puesto completar ista acción. Esviella y torna a intentar-lo.",
"saml-login-not-started": "Iste inicio de sesión SAML no s'ha empecipiau en ista pestanya d'o navegador. Torna a encetar sesión.",
@@ -4444,7 +4461,7 @@
"header-login": "Inicio de sesión por cabeceras HTTP",
"login-setting-clear-secret": "Eliminar a valor alzada en o panel d’administrador",
"login-setting-after-restart": "Tiene efecto dimpués de reiniciar WeKan",
"login-setting-env-only": "Only the server environment sets these. They are shown here read-only.",
"login-setting-env-only": "Nomás l'entorno d'o servidor configura isto. S'amuestra aquí nomás de lectura.",
"email-to-card": "Correu electronico a tarcheta",
"email-to-card-description": "Un correu electronico ninviau a l'adreza d'o tuyo tablero se fa una tarcheta en ixe tablero.",
"email-to-card-admin-enabled": "Recibir correus electronicos en tarchetas",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "عنوان URL لرابط شعار تسجيل الدخول المخصص",
"custom-help-link-url": "رابط صفحة المساعدة المخصص URL",
"text-below-custom-login-logo": "النص أسفل شعار تسجيل الدخول المخصص",
"login-background-color": "لون خلفية صفحة تسجيل الدخول",
"automatic-linked-url-schemes": "مخططات URL المخصصة التي يجب أن تكون قابلة للنقر تلقائياً. مخطط URL واحد لكل سطر",
"automatic-linked-url-schemes-hint": "تكون فارغة افتراضيًا، لذا يمكن النقر على روابط الويب والبريد فقط. يفتح كل مخطط مدرج، مثل thunderlink أو onenote، التطبيق المسجل له. لا تُحوَّل مخططات javascript وdata وvbscript والمخططات المشابهة إلى روابط أبدًا.",
"external-link-pattern": "الربط التلقائي بمتتبع مشاكل خارجي",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "عندما يبدأ عنوان بطاقة جديدة بـ [نص] متبوعًا بنص آخر، يصبح النص علامة للبطاقة (تُنشأ إذا لم تكن في اللوحة علامة بهذا الاسم) ويُحذف من العنوان. عند الإيقاف: يبقى العنوان كما هو.",
"keep-add-card-form-open": "إبقاء نموذج إضافة بطاقة مفتوحًا بعد إضافة بطاقة",
"keep-add-card-form-open-description": "تشغيل: بعد إضافة بطاقة، يبقى النموذج مفتوحًا بعنوان فارغ للبطاقة التالية. إيقاف: يُغلق النموذج بمجرد إضافة البطاقة.",
"show-focus-outline": "إظهار حدود التركيز",
"checklist-ding-sound": "تشغيل صوت عند إتمام عنصر في قائمة المهام",
"checklist-ding-sound-description": "تشغيل نغمة قصيرة عند تحديد عنصر في قائمة المهام. معطّل افتراضيًا.",
"assignee": "المكلّف",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "فشل الاتصال",
"cloud-settings-saved": "تم حفظ إعدادات التخزين",
"cloud-settings-save-failed": "فشل حفظ إعدادات التخزين",
"cloud": "السحابة",
"cloud-settings-desc": "مع WeKan في Weddy، تخدم Cloud Panel العديد من نطاقات WeKan من خادم واحد، ولكل منها بياناته الخاصة. في نطاق Cloud Panel، يدير SuperAdmin النطاقات وCloudFlare.",
"cloud-panel-domain": "نطاق Cloud Panel",
"cloud-superadmin-username": "اسم مستخدم SuperAdmin",
"cloud-saved": "تم الحفظ.",
"cloud-panel-not-allowed": "Cloud Panel مخصصة لـ SuperAdmin فقط، على نطاق Cloud Panel.",
"cloud-customer-name": "اسم العميل",
"cloud-customer-email": "عنوان البريد الإلكتروني للعميل",
"cloud-billing-email": "عنوان البريد الإلكتروني للفوترة",
"cloud-start-date": "تاريخ البدء",
"cloud-end-date": "تاريخ الانتهاء",
"cloudflare-api-key": "مفتاح CloudFlare API",
"cloudflare-api-key-desc": "باستخدام مفتاح CloudFlare API، يحصل النطاق الذي توجد منطقته في حساب CloudFlare الخاص بك على سجل DNS عبر الوكيل وشهادة CloudFlare Origin. تحصل النطاقات الأخرى على شهادة Let's Encrypt. يُحفظ المفتاح على الخادم ولا يُعرض مرة أخرى أبدًا.",
"cloudflare-api-key-set": "تم حفظ مفتاح CloudFlare API.",
"cloudflare-api-key-not-set": "لم يتم حفظ أي مفتاح CloudFlare API: يحصل كل نطاق على شهادة Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "نقل المرفق إلى Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "عنوان URL لرابط شعار تسجيل الدخول المخصص",
"custom-help-link-url": "رابط صفحة المساعدة المخصص URL",
"text-below-custom-login-logo": "النص أسفل شعار تسجيل الدخول المخصص",
"login-background-color": "لون خلفية صفحة تسجيل الدخول",
"automatic-linked-url-schemes": "مخططات URL المخصصة التي يجب أن تكون قابلة للنقر تلقائياً. مخطط URL واحد لكل سطر",
"automatic-linked-url-schemes-hint": "تكون فارغة افتراضيًا، لذا يمكن النقر على روابط الويب والبريد فقط. يفتح كل مخطط مدرج، مثل thunderlink أو onenote، التطبيق المسجل له. لا تُحوَّل مخططات javascript وdata وvbscript والمخططات المشابهة إلى روابط أبدًا.",
"external-link-pattern": "الربط التلقائي بمتتبع مشاكل خارجي",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "عندما يبدأ عنوان بطاقة جديدة بـ [نص] متبوعًا بنص آخر، يصبح النص علامة للبطاقة (تُنشأ إذا لم تكن في اللوحة علامة بهذا الاسم) ويُحذف من العنوان. عند الإيقاف: يبقى العنوان كما هو.",
"keep-add-card-form-open": "إبقاء نموذج إضافة بطاقة مفتوحًا بعد إضافة بطاقة",
"keep-add-card-form-open-description": "تشغيل: بعد إضافة بطاقة، يبقى النموذج مفتوحًا بعنوان فارغ للبطاقة التالية. إيقاف: يُغلق النموذج بمجرد إضافة البطاقة.",
"show-focus-outline": "إظهار حدود التركيز",
"checklist-ding-sound": "تشغيل صوت عند إتمام عنصر في قائمة المهام",
"checklist-ding-sound-description": "تشغيل نغمة قصيرة عند تحديد عنصر في قائمة المهام. معطّل افتراضيًا.",
"assignee": "المكلّف",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "فشل الاتصال",
"cloud-settings-saved": "تم حفظ إعدادات التخزين",
"cloud-settings-save-failed": "فشل حفظ إعدادات التخزين",
"cloud": "السحابة",
"cloud-settings-desc": "مع WeKan في Weddy، تخدم Cloud Panel العديد من نطاقات WeKan من خادم واحد، ولكل منها بياناته الخاصة. في نطاق Cloud Panel، يدير SuperAdmin النطاقات وCloudFlare.",
"cloud-panel-domain": "نطاق Cloud Panel",
"cloud-superadmin-username": "اسم مستخدم SuperAdmin",
"cloud-saved": "تم الحفظ.",
"cloud-panel-not-allowed": "Cloud Panel مخصصة لـ SuperAdmin فقط، على نطاق Cloud Panel.",
"cloud-customer-name": "اسم العميل",
"cloud-customer-email": "عنوان البريد الإلكتروني للعميل",
"cloud-billing-email": "عنوان البريد الإلكتروني للفوترة",
"cloud-start-date": "تاريخ البدء",
"cloud-end-date": "تاريخ الانتهاء",
"cloudflare-api-key": "مفتاح CloudFlare API",
"cloudflare-api-key-desc": "باستخدام مفتاح CloudFlare API، يحصل النطاق الذي توجد منطقته في حساب CloudFlare الخاص بك على سجل DNS عبر الوكيل وشهادة CloudFlare Origin. تحصل النطاقات الأخرى على شهادة Let's Encrypt. يُحفظ المفتاح على الخادم ولا يُعرض مرة أخرى أبدًا.",
"cloudflare-api-key-set": "تم حفظ مفتاح CloudFlare API.",
"cloudflare-api-key-not-set": "لم يتم حفظ أي مفتاح CloudFlare API: يحصل كل نطاق على شهادة Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "نقل المرفق إلى Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "عنوان URL لرابط شعار تسجيل الدخول المخصص",
"custom-help-link-url": "رابط صفحة المساعدة المخصص URL",
"text-below-custom-login-logo": "النص أسفل شعار تسجيل الدخول المخصص",
"login-background-color": "لون خلفية صفحة تسجيل الدخول",
"automatic-linked-url-schemes": "مخططات URL المخصصة التي يجب أن تكون قابلة للنقر تلقائياً. مخطط URL واحد لكل سطر",
"automatic-linked-url-schemes-hint": "تكون فارغة افتراضيًا، لذا يمكن النقر على روابط الويب والبريد فقط. يفتح كل مخطط مدرج، مثل thunderlink أو onenote، التطبيق المسجل له. لا تُحوَّل مخططات javascript وdata وvbscript والمخططات المشابهة إلى روابط أبدًا.",
"external-link-pattern": "الربط التلقائي بمتتبع مشاكل خارجي",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "عندما يبدأ عنوان بطاقة جديدة بـ [نص] متبوعًا بنص آخر، يصبح النص علامة للبطاقة (تُنشأ إذا لم تكن في اللوحة علامة بهذا الاسم) ويُحذف من العنوان. عند الإيقاف: يبقى العنوان كما هو.",
"keep-add-card-form-open": "إبقاء نموذج إضافة بطاقة مفتوحًا بعد إضافة بطاقة",
"keep-add-card-form-open-description": "تشغيل: بعد إضافة بطاقة، يبقى النموذج مفتوحًا بعنوان فارغ للبطاقة التالية. إيقاف: يُغلق النموذج بمجرد إضافة البطاقة.",
"show-focus-outline": "إظهار حدود التركيز",
"checklist-ding-sound": "تشغيل صوت عند إتمام عنصر في قائمة المهام",
"checklist-ding-sound-description": "تشغيل نغمة قصيرة عند تحديد عنصر في قائمة المهام. معطّل افتراضيًا.",
"assignee": "المكلّف",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "فشل الاتصال",
"cloud-settings-saved": "تم حفظ إعدادات التخزين",
"cloud-settings-save-failed": "فشل حفظ إعدادات التخزين",
"cloud": "السحابة",
"cloud-settings-desc": "مع WeKan في Weddy، تخدم Cloud Panel العديد من نطاقات WeKan من خادم واحد، ولكل منها بياناته الخاصة. في نطاق Cloud Panel، يدير SuperAdmin النطاقات وCloudFlare.",
"cloud-panel-domain": "نطاق Cloud Panel",
"cloud-superadmin-username": "اسم مستخدم SuperAdmin",
"cloud-saved": "تم الحفظ.",
"cloud-panel-not-allowed": "Cloud Panel مخصصة لـ SuperAdmin فقط، على نطاق Cloud Panel.",
"cloud-customer-name": "اسم العميل",
"cloud-customer-email": "عنوان البريد الإلكتروني للعميل",
"cloud-billing-email": "عنوان البريد الإلكتروني للفوترة",
"cloud-start-date": "تاريخ البدء",
"cloud-end-date": "تاريخ الانتهاء",
"cloudflare-api-key": "مفتاح CloudFlare API",
"cloudflare-api-key-desc": "باستخدام مفتاح CloudFlare API، يحصل النطاق الذي توجد منطقته في حساب CloudFlare الخاص بك على سجل DNS عبر الوكيل وشهادة CloudFlare Origin. تحصل النطاقات الأخرى على شهادة Let's Encrypt. يُحفظ المفتاح على الخادم ولا يُعرض مرة أخرى أبدًا.",
"cloudflare-api-key-set": "تم حفظ مفتاح CloudFlare API.",
"cloudflare-api-key-not-set": "لم يتم حفظ أي مفتاح CloudFlare API: يحصل كل نطاق على شهادة Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "نقل المرفق إلى Azure Blob Storage",
File diff suppressed because it is too large. Load diff
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "স্বনিৰ্বাচিত লগইন ল'গ' সংযোগৰ URL",
"custom-help-link-url": "স্বনিৰ্বাচিত সহায় সংযোগৰ URL",
"text-below-custom-login-logo": "স্বনিৰ্বাচিত লগইন ল'গ'ৰ তলৰ পাঠ",
"login-background-color": "লগইন পৃষ্ঠাৰ পটভূমিৰ ৰং",
"automatic-linked-url-schemes": "স্বয়ংক্ৰিয়ভাৱে ক্লিক কৰিব পৰা স্বনিৰ্বাচিত URL আঁচনি। প্ৰতি শাৰীত এটা URL আঁচনি",
"automatic-linked-url-schemes-hint": "অবিকল্পিতভাৱে খালী থাকে, সেয়ে কেৱল ৱেব আৰু মেইল লিংকত ক্লিক কৰিব পাৰি। তালিকাভুক্ত প্ৰতিটো স্কীমে, যেনে thunderlink বা onenote, তাৰ বাবে পঞ্জীয়ন কৰা এপ্লিকেচন খোলে। javascript, data, vbscript আৰু একেধৰণৰ স্কীম কেতিয়াও লিংক কৰা নহয়।",
"external-link-pattern": "বাহিৰৰ সমস্যা ট্ৰেকাৰলৈ স্বয়ংক্ৰিয় লিংক",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "নতুন কাৰ্ডৰ শিৰোনাম [লিখনি]ৰে আৰম্ভ হৈ তাৰ পিছত আৰু লিখনি থাকিলে, লিখনি কাৰ্ডখনৰ লেবেল হয় (ফলকত সেই নামৰ লেবেল নাথাকিলে সৃষ্টি কৰা হয়) আৰু শিৰোনামৰ পৰা আঁতৰোৱা হয়। বন্ধ: শিৰোনাম যেনে আছে তেনেকৈ থাকে।",
"keep-add-card-form-open": "কাৰ্ড যোগ কৰাৰ পিছত কাৰ্ড যোগ কৰক ফৰ্মখন খোলা ৰাখক",
"keep-add-card-form-open-description": "সক্ৰিয়: কাৰ্ড যোগ কৰাৰ পিছত পৰৱৰ্তী কাৰ্ডৰ বাবে খালী শিৰোনামৰ সৈতে ফৰ্মখন খোলা থাকে। নিষ্ক্ৰিয়: কাৰ্ডখন যোগ হোৱাৰ লগে লগে ফৰ্মখন বন্ধ হয়।",
"show-focus-outline": "ফোকাছ আউটলাইন দেখুৱাওক",
"checklist-ding-sound": "চেকলিষ্ট বস্তু সম্পূৰ্ণ হ'লে শব্দ বজাওক",
"checklist-ding-sound-description": "চেকলিষ্ট বস্তু চেক কৰাৰ সময়ত চুটি শব্দ বজাওক। অবিকল্পিতভাৱে অফ।",
"assignee": "দায়িত্বপ্ৰাপ্ত",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "সংযোগ বিফল",
"cloud-settings-saved": "সংৰক্ষণৰ ছেটিংছ সংৰক্ষণ কৰা হ'ল",
"cloud-settings-save-failed": "সংৰক্ষণৰ ছেটিংছ সংৰক্ষণ কৰিব পৰা নগ'ল",
"cloud": "ডাৱৰ",
"cloud-settings-desc": "Weddyত WeKanৰ সৈতে, Cloud Panel এ এটা ছাৰ্ভাৰৰ পৰা বহুতো WeKan ড'মেইন সেৱা আগবঢ়ায়, প্ৰতিটোৰে নিজা ডাটা থাকে। Cloud Panel ড'মেইনত SuperAdmin এ ড'মেইনসমূহ আৰু CloudFlare পৰিচালনা কৰে।",
"cloud-panel-domain": "Cloud Panel ড'মেইন",
"cloud-superadmin-username": "SuperAdminৰ ব্যৱহাৰকাৰীৰ নাম",
"cloud-saved": "সংৰক্ষণ কৰা হ'ল।",
"cloud-panel-not-allowed": "Cloud Panel কেৱল SuperAdminৰ বাবে, Cloud Panel ড'মেইনত।",
"cloud-customer-name": "গ্ৰাহকৰ নাম",
"cloud-customer-email": "গ্ৰাহকৰ ইমেইল ঠিকনা",
"cloud-billing-email": "বিলিং ইমেইল ঠিকনা",
"cloud-start-date": "আৰম্ভণিৰ তাৰিখ",
"cloud-end-date": "শেষৰ তাৰিখ",
"cloudflare-api-key": "CloudFlare API চাবি",
"cloudflare-api-key-desc": "CloudFlare API চাবিৰ সৈতে, যি ড'মেইনৰ জ'ন আপোনাৰ CloudFlare একাউণ্টত আছে, সেইটোৱে প্ৰক্সি কৰা DNS ৰেকৰ্ড আৰু CloudFlare Origin প্ৰমাণপত্ৰ পায়। অন্য ড'মেইনে Let's Encrypt প্ৰমাণপত্ৰ পায়। চাবিটো ছাৰ্ভাৰত সংৰক্ষণ কৰা হয় আৰু আৰু কেতিয়াও দেখুওৱা নহয়।",
"cloudflare-api-key-set": "এটা CloudFlare API চাবি সংৰক্ষণ কৰা হৈছে।",
"cloudflare-api-key-not-set": "কোনো CloudFlare API চাবি সংৰক্ষণ কৰা হোৱা নাই: প্ৰতিটো ড'মেইনে Let's Encrypt প্ৰমাণপত্ৰ পায়।",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "সংলগ্নক Azure Blob Storage-লৈ স্থানান্তৰ কৰক",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL del enllaz del logotipu personalizáu d’aniciu de sesión",
"custom-help-link-url": "URL del enllaz d’ayuda personalizáu",
"text-below-custom-login-logo": "Testu debaxo del logotipu personalizáu d’aniciu de sesión",
"login-background-color": "Color de fondu de la páxina d'aniciu de sesión",
"automatic-linked-url-schemes": "Esquemes d’URL personalizaos que tienen de poder calcase automáticamente. Un esquema d’URL per llinia.",
"automatic-linked-url-schemes-hint": "Balero por defeutu, polo que namás se pue calcar nos enllaces web y de corréu. Cada esquema llistáu, por exemplu thunderlink o onenote, abre l'aplicación rexistrada pa él. javascript, data, vbscript y esquemes asemeyaos nunca se convierten n'enllaces.",
"external-link-pattern": "Enllaz automáticu a un rastreador d'asuntos esternu",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Cuando'l títulu d'una tarxeta nueva entama con [Testu] siguíu de más testu, Testu conviértese nuna etiqueta de la tarxeta (críase si'l tableru nun tien nenguna con esi nome) y quítase del títulu. Desactiváu: el títulu caltiénse como ta.",
"keep-add-card-form-open": "Caltener abiertu'l formulariu d'amestar tarxeta dempués d'amestar una tarxeta",
"keep-add-card-form-open-description": "Activáu: dempués d'amestar una tarxeta, el formulariu sigue abiertu con un títulu baleru pa la tarxeta siguiente. Desactiváu: el formulariu ciérrase en cuantes s'amiesta la tarxeta.",
"show-focus-outline": "Amosar el contornu del focu",
"checklist-ding-sound": "Sonar cuando se completa un elementu de llista de verificación",
"checklist-ding-sound-description": "Sonar un tonu curtiu cuando marques un elementu de llista de verificación. Desactiváu por defeutu.",
"assignee": "Asignado",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Falló la conexón",
"cloud-settings-saved": "Guardáronse les preferencies d'almacenamientu",
"cloud-settings-save-failed": "Nun se pudieron guardar les preferencies d'almacenamientu",
"cloud": "Ñube",
"cloud-settings-desc": "Con WeKan en Weddy, el Cloud Panel sirve munchos dominios de WeKan dende un únicu sirvidor, cada ún colos sos datos. Nel dominiu del Cloud Panel, el SuperAdmin xestiona los dominios y CloudFlare.",
"cloud-panel-domain": "Dominiu del Cloud Panel",
"cloud-superadmin-username": "Nome d'usuariu del SuperAdmin",
"cloud-saved": "Guardóse.",
"cloud-panel-not-allowed": "El Cloud Panel ye namái pal SuperAdmin, nel dominiu del Cloud Panel.",
"cloud-customer-name": "Nome del veceru",
"cloud-customer-email": "Direición de corréu del veceru",
"cloud-billing-email": "Direición de corréu de facturación",
"cloud-start-date": "Data d'aniciu",
"cloud-end-date": "Data de fin",
"cloudflare-api-key": "Clave API de CloudFlare",
"cloudflare-api-key-desc": "Con una clave API de CloudFlare, un dominiu que tenga la so zona na to cuenta de CloudFlare consigue un rexistru DNS con proxy y un certificáu CloudFlare Origin. Los otros dominios consiguen un certificáu Let's Encrypt. La clave guárdase nel sirvidor y enxamás nun se vuelve amosar.",
"cloudflare-api-key-set": "Guardóse una clave API de CloudFlare.",
"cloudflare-api-key-not-set": "Nun se guardó nenguna clave API de CloudFlare: cada dominiu consigue un certificáu Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Mover l'adjuntu a Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Custom Login Logo chinu URL",
"custom-help-link-url": "Custom Help chinu URL",
"text-below-custom-login-logo": "qillqa below Custom Login Logo",
"login-background-color": "Login panka qhipa color",
"automatic-linked-url-schemes": "Aymar aruna: Custom URL Schemes which should automatically be clickable. One URL Scheme per line",
"automatic-linked-url-schemes-hint": "Qalltanxa chʼusawa, ukatwa web ukat correo electrónico chinuwinakaki limtʼasma. Listankir sapa esquema, sañäni thunderlink jan ukax onenote, jupatak qillqantat programa jistʼari. javascript, data, vbscript ukat ukham esquemanakax janipuniw chinuwir tukuyatäkiti.",
"external-link-pattern": "Autoenlace hacia jach'a tracker jan p'ampachaña",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Machaqa kartan p'iqi sutipax [Qillqa] ukamp qalltani ukatsti juk'amp qillqax arkani ukhaxa, Qillqax kartan unanchapaw tuku (pirqan uka sutin unanchax janiw utjkchi ukhax lurataw) ukatsti p'iqi sutit apsutaw. Jist'arata: p'iqi sutix kunjamatix ukhamaw qhipari.",
"keep-add-card-form-open": "Karta yapañ formulario jist'aratapa uñjaña, karta yapatatxa",
"keep-add-card-form-open-description": "Naktayata: karta yapatatxa, formulariox jist'aratakiw qhipa kartataki ch'usa sutimpi. Jiwt'ayata: formulariox jist'antasiwa karta yapatatxa.",
"show-focus-outline": "Qhanstayaña muyu uñachtʼayaña",
"checklist-ding-sound": "Checklist amtata phuqhasitaxa qhanañchaña uñstayaña",
"checklist-ding-sound-description": "Checklist amtata qillqasina taqi kunaru jisk'a qhanañcha uñstayi. Jan apnaqata.",
"assignee": "Lurañataki churata",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Connection jani phuqhata",
"cloud-settings-saved": "Storage wakichäwinaka saved",
"cloud-settings-save-failed": "jani phuqhata ukaru imaña storage wakichäwinaka",
"cloud": "Qinaya",
"cloud-settings-desc": "WeKan Weddy ukan utjipanxa, Cloud Panel ukax walja WeKan dominionak mä servidorat luqtaraki, sapakis pachpa datonakanïtap. Cloud Panel Dominio ukanxa, SuperAdmin ukaw dominionak CloudFlare ukamp apnaqaraki.",
"cloud-panel-domain": "Cloud Panel Dominio",
"cloud-superadmin-username": "SuperAdmin apnaqirin sutipa",
"cloud-saved": "Imatäxiwa.",
"cloud-panel-not-allowed": "Cloud Panel ukax SuperAdmin ukatakikiw, Cloud Panel Dominio ukana.",
"cloud-customer-name": "Alakipirin sutipa",
"cloud-customer-email": "Alakipirin email chiqapa",
"cloud-billing-email": "Payllañ email chiqa",
"cloud-start-date": "Qallta uru",
"cloud-end-date": "Tukuya uru",
"cloudflare-api-key": "CloudFlare API llawi",
"cloudflare-api-key-desc": "CloudFlare API llawimpixa, CloudFlare cuentaman zonap utjki uka dominiox proxy DNS qillqatampi CloudFlare Origin certificadompi katuqi. Yaqha dominionakax Let's Encrypt certificado katuqapxi. Llawix servidoran imatawa, janiw mayamp uñachayatäkiti.",
"cloudflare-api-key-set": "CloudFlare API llawiw imataxi.",
"cloudflare-api-key-not-set": "Janiw kuna CloudFlare API llawis imatäkiti: sapa dominiox Let's Encrypt certificado katuqi.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "unxtayaña apkatata ukaru Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Fərdi Giriş Loqo Linki URL",
"custom-help-link-url": "Fərdi Yardım Linki URL",
"text-below-custom-login-logo": "Xüsusi Giriş Loqosunun altındakı mətn",
"login-background-color": "Giriş səhifəsinin fon rəngi",
"automatic-linked-url-schemes": "Avtomatik olaraq klik edilə bilən fərdi URL sxemləri. Hər sətir üçün bir URL sxemi",
"automatic-linked-url-schemes-hint": "Standart olaraq boşdur, buna görə yalnız veb və poçt keçidləri kliklənə bilər. Siyahıdakı hər sxem, məsələn, thunderlink və ya onenote, onun üçün qeydiyyatdan keçmiş tətbiqi açır. javascript, data, vbscript və oxşar sxemlər heç vaxt keçidə çevrilmir.",
"external-link-pattern": "Xarici xəta izləyicisinə avtomatik keçid",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Yeni kartın başlığı [Mətn] ilə başlayıb ardınca başqa mətn gəldikdə, Mətn kartın etiketi olur (lövhədə bu adda etiket yoxdursa, yaradılır) və başlıqdan silinir. Söndürülüb: başlıq olduğu kimi saxlanılır.",
"keep-add-card-form-open": "Kart əlavə edildikdən sonra Kart əlavə et formasını açıq saxla",
"keep-add-card-form-open-description": "Aktiv: kart əlavə edildikdən sonra forma növbəti kart üçün boş başlıqla açıq qalır. Deaktiv: kart əlavə edilən kimi forma bağlanır.",
"show-focus-outline": "Fokus konturunu göstər",
"checklist-ding-sound": "Yoxlama siyahısı elementi tamamlandıqda səs çal",
"checklist-ding-sound-description": "Yoxlama siyahısı elementini işarələyəndə qısa siqnal səsi çal. Standart olaraq söndürülüb.",
"assignee": "Təyin edilmiş şəxs",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Bağlantı uğursuz oldu",
"cloud-settings-saved": "Saxlama parametrləri saxlanıldı",
"cloud-settings-save-failed": "Saxlama parametrləri saxlanıla bilmədi",
"cloud": "Bulud",
"cloud-settings-desc": "Weddy-də WeKan ilə Cloud Panel bir serverdən çoxlu WeKan domenlərinə xidmət edir, hər birinin öz məlumatları var. Cloud Panel domenində SuperAdmin domenləri və CloudFlare-i idarə edir.",
"cloud-panel-domain": "Cloud Panel domeni",
"cloud-superadmin-username": "SuperAdmin istifadəçi adı",
"cloud-saved": "Saxlanıldı.",
"cloud-panel-not-allowed": "Cloud Panel yalnız SuperAdmin üçündür, Cloud Panel domenində.",
"cloud-customer-name": "Müştərinin adı",
"cloud-customer-email": "Müştərinin e-poçt ünvanı",
"cloud-billing-email": "Hesab-faktura e-poçt ünvanı",
"cloud-start-date": "Başlama tarixi",
"cloud-end-date": "Bitmə tarixi",
"cloudflare-api-key": "CloudFlare API açarı",
"cloudflare-api-key-desc": "CloudFlare API açarı ilə zonası CloudFlare hesabınızda olan domen proksilənmiş DNS qeydi və CloudFlare Origin sertifikatı alır. Digər domenlər Let's Encrypt sertifikatı alır. Açar serverdə saxlanılır və bir daha göstərilmir.",
"cloudflare-api-key-set": "CloudFlare API açarı saxlanılıb.",
"cloudflare-api-key-not-set": "Heç bir CloudFlare API açarı saxlanılmayıb: hər domen Let's Encrypt sertifikatı alır.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Qoşmanı Azure Blob Storage-ə köçür",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Fərdi Giriş Loqo Linki URL",
"custom-help-link-url": "Fərdi Yardım Linki URL",
"text-below-custom-login-logo": "Xüsusi Giriş Loqosunun altındakı mətn",
"login-background-color": "Giriş səhifəsinin fon rəngi",
"automatic-linked-url-schemes": "Avtomatik olaraq klik edilə bilən fərdi URL sxemləri. Hər sətir üçün bir URL sxemi",
"automatic-linked-url-schemes-hint": "Standart olaraq boşdur, buna görə yalnız veb və poçt keçidləri kliklənə bilər. Siyahıdakı hər sxem, məsələn, thunderlink və ya onenote, onun üçün qeydiyyatdan keçmiş tətbiqi açır. javascript, data, vbscript və oxşar sxemlər heç vaxt keçidə çevrilmir.",
"external-link-pattern": "Xarici xəta izləyicisinə avtomatik keçid",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Yeni kartın başlığı [Mətn] ilə başlayıb ardınca başqa mətn gəldikdə, Mətn kartın etiketi olur (lövhədə bu adda etiket yoxdursa, yaradılır) və başlıqdan silinir. Söndürülüb: başlıq olduğu kimi saxlanılır.",
"keep-add-card-form-open": "Kart əlavə edildikdən sonra Kart əlavə et formasını açıq saxla",
"keep-add-card-form-open-description": "Aktiv: kart əlavə edildikdən sonra forma növbəti kart üçün boş başlıqla açıq qalır. Deaktiv: kart əlavə edilən kimi forma bağlanır.",
"show-focus-outline": "Fokus konturunu göstər",
"checklist-ding-sound": "Yoxlama siyahısı elementi tamamlandıqda səs çal",
"checklist-ding-sound-description": "Yoxlama siyahısı elementini işarələyəndə qısa siqnal səsi çal. Standart olaraq söndürülüb.",
"assignee": "Təyin edilmiş şəxs",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Bağlantı uğursuz oldu",
"cloud-settings-saved": "Saxlama parametrləri saxlanıldı",
"cloud-settings-save-failed": "Saxlama parametrləri saxlanıla bilmədi",
"cloud": "Bulud",
"cloud-settings-desc": "Weddy-də WeKan ilə Cloud Panel bir serverdən çoxlu WeKan domenlərinə xidmət edir, hər birinin öz məlumatları var. Cloud Panel domenində SuperAdmin domenləri və CloudFlare-i idarə edir.",
"cloud-panel-domain": "Cloud Panel domeni",
"cloud-superadmin-username": "SuperAdmin istifadəçi adı",
"cloud-saved": "Saxlanıldı.",
"cloud-panel-not-allowed": "Cloud Panel yalnız SuperAdmin üçündür, Cloud Panel domenində.",
"cloud-customer-name": "Müştərinin adı",
"cloud-customer-email": "Müştərinin e-poçt ünvanı",
"cloud-billing-email": "Hesab-faktura e-poçt ünvanı",
"cloud-start-date": "Başlama tarixi",
"cloud-end-date": "Bitmə tarixi",
"cloudflare-api-key": "CloudFlare API açarı",
"cloudflare-api-key-desc": "CloudFlare API açarı ilə zonası CloudFlare hesabınızda olan domen proksilənmiş DNS qeydi və CloudFlare Origin sertifikatı alır. Digər domenlər Let's Encrypt sertifikatı alır. Açar serverdə saxlanılır və bir daha göstərilmir.",
"cloudflare-api-key-set": "CloudFlare API açarı saxlanılıb.",
"cloudflare-api-key-not-set": "Heç bir CloudFlare API açarı saxlanılmayıb: hər domen Let's Encrypt sertifikatı alır.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Qoşmanı Azure Blob Storage-ə köçür",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Fərdi Giriş Loqo Linki URL",
"custom-help-link-url": "Fərdi Yardım Linki URL",
"text-below-custom-login-logo": "Xüsusi Giriş Loqosunun altındakı mətn",
"login-background-color": "Giriş səhifəsinin fon rəngi",
"automatic-linked-url-schemes": "Avtomatik olaraq klik edilə bilən fərdi URL sxemləri. Hər sətir üçün bir URL sxemi",
"automatic-linked-url-schemes-hint": "Standart olaraq boşdur, buna görə yalnız veb və poçt keçidləri kliklənə bilər. Siyahıdakı hər sxem, məsələn, thunderlink və ya onenote, onun üçün qeydiyyatdan keçmiş tətbiqi açır. javascript, data, vbscript və oxşar sxemlər heç vaxt keçidə çevrilmir.",
"external-link-pattern": "Xarici xəta izləyicisinə avtomatik keçid",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Yeni kartın başlığı [Mətn] ilə başlayıb ardınca başqa mətn gəldikdə, Mətn kartın etiketi olur (lövhədə bu adda etiket yoxdursa, yaradılır) və başlıqdan silinir. Söndürülüb: başlıq olduğu kimi saxlanılır.",
"keep-add-card-form-open": "Kart əlavə edildikdən sonra Kart əlavə et formasını açıq saxla",
"keep-add-card-form-open-description": "Aktiv: kart əlavə edildikdən sonra forma növbəti kart üçün boş başlıqla açıq qalır. Deaktiv: kart əlavə edilən kimi forma bağlanır.",
"show-focus-outline": "Fokus konturunu göstər",
"checklist-ding-sound": "Yoxlama siyahısı elementi tamamlandıqda səs çal",
"checklist-ding-sound-description": "Yoxlama siyahısı elementini işarələyəndə qısa siqnal səsi çal. Standart olaraq söndürülüb.",
"assignee": "Təyin edilmiş şəxs",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Bağlantı uğursuz oldu",
"cloud-settings-saved": "Saxlama parametrləri saxlanıldı",
"cloud-settings-save-failed": "Saxlama parametrləri saxlanıla bilmədi",
"cloud": "Bulud",
"cloud-settings-desc": "Weddy-də WeKan ilə Cloud Panel bir serverdən çoxlu WeKan domenlərinə xidmət edir, hər birinin öz məlumatları var. Cloud Panel domenində SuperAdmin domenləri və CloudFlare-i idarə edir.",
"cloud-panel-domain": "Cloud Panel domeni",
"cloud-superadmin-username": "SuperAdmin istifadəçi adı",
"cloud-saved": "Saxlanıldı.",
"cloud-panel-not-allowed": "Cloud Panel yalnız SuperAdmin üçündür, Cloud Panel domenində.",
"cloud-customer-name": "Müştərinin adı",
"cloud-customer-email": "Müştərinin e-poçt ünvanı",
"cloud-billing-email": "Hesab-faktura e-poçt ünvanı",
"cloud-start-date": "Başlama tarixi",
"cloud-end-date": "Bitmə tarixi",
"cloudflare-api-key": "CloudFlare API açarı",
"cloudflare-api-key-desc": "CloudFlare API açarı ilə zonası CloudFlare hesabınızda olan domen proksilənmiş DNS qeydi və CloudFlare Origin sertifikatı alır. Digər domenlər Let's Encrypt sertifikatı alır. Açar serverdə saxlanılır və bir daha göstərilmir.",
"cloudflare-api-key-set": "CloudFlare API açarı saxlanılıb.",
"cloudflare-api-key-not-set": "Heç bir CloudFlare API açarı saxlanılmayıb: hər domen Let's Encrypt sertifikatı alır.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Qoşmanı Azure Blob Storage-ə köçür",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Махсус инеү логотип һылтанмаһының URL-ы",
"custom-help-link-url": "Махсус ярҙам һылтанмаһының URL-ы",
"text-below-custom-login-logo": "Махсус инеү логотибы аҫтындағы текст",
"login-background-color": "Керү битенең фон төсө",
"automatic-linked-url-schemes": "Автоматик баҫыла торған махсус URL схемалары. Һәр юлға бер URL схемаһы",
"automatic-linked-url-schemes-hint": "Ғәҙәттә буш, шуға күрә веб һәм почта һылтанмаларына ғына баҫып була. Исемлектәге һәр схема, мәҫәлән thunderlink йәки onenote, уның өсөн теркәлгән ҡушымтаны аса. javascript, data, vbscript һәм оҡшаш схемалар бер ҡасан да һылтанмаға әйләндерелмәй.",
"external-link-pattern": "Тышҡы проблема күҙәтеүсеһенә автоматик һылтанма",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Яңы карточканың баш һүҙе [Текст] менән башланып, артынан башҡа текст килһә, Текст карточканың билдәһе була (тактала был исемле билдә булмаһа, булдырыла) һәм баш һүҙҙән алына. Һүндерелгән: баш һүҙ нисек бар, шулай ҡала.",
"keep-add-card-form-open": "Карточка өҫтәгәндән һуң «Карточка өҫтәү» формаһын асыҡ ҡалдырырға",
"keep-add-card-form-open-description": "Ҡабыҙылған: карточка өҫтәлгәндән һуң форма киләһе карточка өсөн буш исем менән асыҡ ҡала. Һүндерелгән: карточка өҫтәлеү менән форма ябыла.",
"show-focus-outline": "Фокус рамкаһын күрһәтергә",
"checklist-ding-sound": "Тикшереү исемлеге пункты тамамланғанда тауыш уйнат",
"checklist-ding-sound-description": "Тикшереү исемлегендәге пунктты билдәләгәндә кыска тауыш уйнат. Стандарт буйынса һүнгән.",
"assignee": "Яуаплы",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Тоташыу уңышһыҙ",
"cloud-settings-saved": "Һаҡлау көйләүҙәре һаҡланды",
"cloud-settings-save-failed": "Һаҡлау көйләүҙәрен һаҡлап булманы",
"cloud": "Болот",
"cloud-settings-desc": "Weddy-ҙағы WeKan менән Cloud Panel бер серверҙан күп WeKan домендарына хеҙмәт күрһәтә, һәр береһенең үҙ мәғлүмәттәре бар. Cloud Panel доменында SuperAdmin домендарҙы һәм CloudFlare-ҙы идара итә.",
"cloud-panel-domain": "Cloud Panel домены",
"cloud-superadmin-username": "SuperAdmin ҡулланыусы исеме",
"cloud-saved": "Һаҡланды.",
"cloud-panel-not-allowed": "Cloud Panel тик SuperAdmin өсөн генә, Cloud Panel доменында.",
"cloud-customer-name": "Клиенттың исеме",
"cloud-customer-email": "Клиенттың электрон почта адресы",
"cloud-billing-email": "Иҫәп-фактура өсөн электрон почта адресы",
"cloud-start-date": "Башланыу көнө",
"cloud-end-date": "Тамамланыу көнө",
"cloudflare-api-key": "CloudFlare API асҡысы",
"cloudflare-api-key-desc": "CloudFlare API асҡысы менән зонаһы һеҙҙең CloudFlare иҫәп яҙмаһында булған домен прокси аша DNS яҙмаһын һәм CloudFlare Origin сертификатын ала. Башҡа домендар Let's Encrypt сертификатын ала. Асҡыс серверҙа һаҡлана һәм бүтән бер ҡасан да күрһәтелмәй.",
"cloudflare-api-key-set": "CloudFlare API асҡысы һаҡланған.",
"cloudflare-api-key-not-set": "Бер ниндәй ҙә CloudFlare API асҡысы һаҡланмаған: һәр домен Let's Encrypt сертификатын ала.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Ҡушымтаны Azure Blob Storage-ға күсерергә",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL спасылкі карыстальніцкага лагатыпа ўваходу",
"custom-help-link-url": "URL карыстальніцкай спасылкі даведкі",
"text-below-custom-login-logo": "Тэкст пад карыстальніцкім лагатыпам уваходу",
"login-background-color": "Колер фону старонкі ўваходу",
"automatic-linked-url-schemes": "Карыстальніцкія схемы URL, якія павінны аўтаматычна станавіцца націскальнымі. Адна схема URL на радок",
"automatic-linked-url-schemes-hint": "Прадвызначана пустое, таму націскаць можна толькі на вэб-спасылкі і спасылкі электроннай пошты. Кожная пералічаная схема, напрыклад thunderlink або onenote, адкрывае зарэгістраваную для яе праграму. Для схем javascript, data, vbscript і падобных спасылкі ніколі не ствараюцца.",
"external-link-pattern": "Аўтаспасылка на знешні трэкер задач",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Калі назва новай карткі пачынаецца з [Тэкст], за якім ідзе іншы тэкст, Тэкст становіцца меткай карткі (ствараецца, калі на дошцы няма меткі з такой назвай) і выдаляецца з назвы. Выключана: назва застаецца як ёсць.",
"keep-add-card-form-open": "Пакідаць форму «Дадаць картку» адкрытай пасля дадання карткі",
"keep-add-card-form-open-description": "Уключана: пасля дадання карткі форма застаецца адкрытай з пустой назвай для наступнай карткі. Выключана: форма закрываецца, як толькі картка дададзена.",
"show-focus-outline": "Паказваць контур фокусу",
"checklist-ding-sound": "Прайграваць гук пры выкананні пункта спіса",
"checklist-ding-sound-description": "Прайграваць кароткі гук пры адзначэнні пункта спіса задач. Па змаўчанні выключана.",
"assignee": "Прызначаны",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Падключэнне не ўдалося",
"cloud-settings-saved": "Налады сховішча захаваны",
"cloud-settings-save-failed": "Не ўдалося захаваць налады сховішча",
"cloud": "Воблака",
"cloud-settings-desc": "З WeKan у Weddy Cloud Panel абслугоўвае шмат даменаў WeKan з аднаго сервера, кожны са сваімі дадзенымі. На дамене Cloud Panel SuperAdmin кіруе даменамі і CloudFlare.",
"cloud-panel-domain": "Дамен Cloud Panel",
"cloud-superadmin-username": "Імя карыстальніка SuperAdmin",
"cloud-saved": "Захавана.",
"cloud-panel-not-allowed": "Cloud Panel толькі для SuperAdmin, на дамене Cloud Panel.",
"cloud-customer-name": "Імя кліента",
"cloud-customer-email": "Адрас электроннай пошты кліента",
"cloud-billing-email": "Адрас электроннай пошты для рахункаў",
"cloud-start-date": "Дата пачатку",
"cloud-end-date": "Дата заканчэння",
"cloudflare-api-key": "Ключ CloudFlare API",
"cloudflare-api-key-desc": "З ключом CloudFlare API дамен, зона якога знаходзіцца ў вашым уліковым запісе CloudFlare, атрымлівае праксіраваны запіс DNS і сертыфікат CloudFlare Origin. Іншыя дамены атрымліваюць сертыфікат Let's Encrypt. Ключ захоўваецца на серверы і больш ніколі не паказваецца.",
"cloudflare-api-key-set": "Ключ CloudFlare API захаваны.",
"cloudflare-api-key-not-set": "Ключ CloudFlare API не захаваны: кожны дамен атрымлівае сертыфікат Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Перамясціць укладанне ў Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL на връзката за персонализираното лого при вход",
"custom-help-link-url": "URL на персонализираната връзка за помощ",
"text-below-custom-login-logo": "Текст под персонализираното лого при вход",
"login-background-color": "Цвят на фона на страницата за вход",
"automatic-linked-url-schemes": "Персонализирани URL схеми, които автоматично стават активни връзки. По една URL схема на ред",
"automatic-linked-url-schemes-hint": "По подразбиране е празно, така че могат да се отварят само уеб връзки и връзки за имейл. Всяка изброена схема, например thunderlink или onenote, отваря приложението, регистрирано за нея. javascript, data, vbscript и подобни схеми никога не стават активни връзки.",
"external-link-pattern": "Автоматично свързване с външна система за проследяване на проблеми",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Когато заглавието на нова карта започва с [Текст], последван от още текст, Текст става етикет на картата (създава се, ако таблото няма етикет с това име) и се премахва от заглавието. Изключено: заглавието се запазва както е.",
"keep-add-card-form-open": "Формата „Добави карта“ да остава отворена след добавяне на карта",
"keep-add-card-form-open-description": "Включено: след добавяне на карта формата остава отворена с празно заглавие за следващата карта. Изключено: формата се затваря веднага щом картата бъде добавена.",
"show-focus-outline": "Показване на контура на фокуса",
"checklist-ding-sound": "Изпълни звук, когато елемент от списъка за проверка е завършен",
"checklist-ding-sound-description": "Изпълни кратък звук при отбелязване на елемент от списъка за проверка. По подразбиране е изключено.",
"assignee": "Изпълнител",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Неуспешна връзка",
"cloud-settings-saved": "Настройките на хранилището са запазени",
"cloud-settings-save-failed": "Неуспешно запазване на настройките на хранилището",
"cloud": "Облак",
"cloud-settings-desc": "С WeKan в Weddy Cloud Panel обслужва много домейни на WeKan от един сървър, всеки със свои собствени данни. На домейна на Cloud Panel SuperAdmin управлява домейните и CloudFlare.",
"cloud-panel-domain": "Домейн на Cloud Panel",
"cloud-superadmin-username": "Потребителско име на SuperAdmin",
"cloud-saved": "Запазено.",
"cloud-panel-not-allowed": "Cloud Panel е само за SuperAdmin, на домейна на Cloud Panel.",
"cloud-customer-name": "Име на клиента",
"cloud-customer-email": "Имейл адрес на клиента",
"cloud-billing-email": "Имейл адрес за фактуриране",
"cloud-start-date": "Начална дата",
"cloud-end-date": "Крайна дата",
"cloudflare-api-key": "CloudFlare API ключ",
"cloudflare-api-key-desc": "С CloudFlare API ключ домейн, чиято зона е във вашия CloudFlare акаунт, получава проксиран DNS запис и CloudFlare Origin сертификат. Другите домейни получават Let's Encrypt сертификат. Ключът се запазва на сървъра и никога повече не се показва.",
"cloudflare-api-key-set": "CloudFlare API ключ е запазен.",
"cloudflare-api-key-not-set": "Няма запазен CloudFlare API ключ: всеки домейн получава Let's Encrypt сертификат.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Премести прикачения файл в Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "मनपसंद लॉग इन लोगो कड़ी URL",
"custom-help-link-url": "मनपसंद मदद कड़ी URL",
"text-below-custom-login-logo": "मनपसंद लॉग इन लोगो के नीचे लिखाई",
"login-background-color": "लॉगिन पन्ना के पृष्ठभूमि रंग",
"automatic-linked-url-schemes": "मनपसंद URL योजना जे अपने-आप क्लिक करे लायक होखे। हर पाँति में एगो URL योजना",
"automatic-linked-url-schemes-hint": "डिफॉल्ट रूप से खाली बा, एहसे खाली वेब आ ईमेल के लिंक पर क्लिक कइल जा सकेला। सूची में दिहल हर लिंक स्कीम, जइसे thunderlink भा onenote, अपना खातिर पंजीकृत ऐप खोलेला। javascript, data, vbscript आ एह तरह के स्कीम के कबो क्लिक करे लायक लिंक ना बनावल जाला।",
"external-link-pattern": "बाहरी इश्यू ट्रैकर से खुदे-खुद लिंक",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "जब नया कार्ड के शीर्षक [पाठ] से शुरू होखे आ ओकरा बाद अउरी लिखल होखे, त पाठ कार्ड के लेबल बन जाला (अगर बोर्ड में ओह नाम के लेबल ना होखे त बनावल जाला) आ शीर्षक से हटा दिहल जाला। बंद: शीर्षक जइसन बा ओइसने रहेला।",
"keep-add-card-form-open": "कार्ड जोड़ला के बाद कार्ड जोड़े वाला फॉर्म खुलल रखीं",
"keep-add-card-form-open-description": "चालू: कार्ड जोड़ला के बाद, अगिला कार्ड खातिर खाली शीर्षक के साथे फॉर्म खुलल रहेला। बंद: कार्ड जुड़ते फॉर्म बंद हो जाला।",
"show-focus-outline": "फोकस आउटलाइन देखाईं",
"checklist-ding-sound": "चेकलिस्ट आइटम पूरा होखला पर आवाज बजाईं",
"checklist-ding-sound-description": "चेकलिस्ट आइटम चेक करला पर छोट आवाज बजाईं। डिफॉल्ट रूप से बंद।",
"assignee": "जिम्मेदार",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "जोड़ असफल भइल",
"cloud-settings-saved": "भंडार सेटिंग सहेज दिहल गइल",
"cloud-settings-save-failed": "भंडार सेटिंग सहेजल असफल भइल",
"cloud": "बादल",
"cloud-settings-desc": "Weddy में WeKan के साथ, Cloud Panel एगो सर्वर से बहुत सारा WeKan डोमेन चलावेला, हर एक के आपन डेटा होला। Cloud Panel डोमेन पर SuperAdmin डोमेन आ CloudFlare के सँभारेला।",
"cloud-panel-domain": "Cloud Panel डोमेन",
"cloud-superadmin-username": "SuperAdmin उपयोगकर्ता नाम",
"cloud-saved": "सहेजा गइल।",
"cloud-panel-not-allowed": "Cloud Panel खाली SuperAdmin खातिर बा, Cloud Panel डोमेन पर।",
"cloud-customer-name": "ग्राहक के नाम",
"cloud-customer-email": "ग्राहक के ईमेल पता",
"cloud-billing-email": "बिलिंग ईमेल पता",
"cloud-start-date": "शुरुआत के तारीख",
"cloud-end-date": "अंत के तारीख",
"cloudflare-api-key": "CloudFlare API कुंजी",
"cloudflare-api-key-desc": "CloudFlare API कुंजी से, जवना डोमेन के ज़ोन रउआ CloudFlare खाता में बा, ओकरा प्रॉक्सी वाला DNS रिकॉर्ड आ CloudFlare Origin प्रमाणपत्र मिलेला। दोसर डोमेन के Let's Encrypt प्रमाणपत्र मिलेला। कुंजी सर्वर पर सहेजल जाला आ फेर कबो ना देखावल जाला।",
"cloudflare-api-key-set": "एगो CloudFlare API कुंजी सहेजल बा।",
"cloudflare-api-key-not-set": "कवनो CloudFlare API कुंजी सहेजल नइखे: हर डोमेन के Let's Encrypt प्रमाणपत्र मिलेला।",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "संलग्नक Azure Blob Storage में सरकाईं",
+240 -223
View File
@@ -227,20 +227,20 @@
"attachment-soft-delete-pop": "Atajmen ia i aot long kad ia, be fael blong hem i stap yet. Eni man we i save jenisim kad ia i save putum bak long histri blong kad.",
"attachmentDeletePopup-title": "Diletem Fael we i joen?",
"attachments": "Ol atajmen",
"attach-card": "Attach card",
"attachCardPopup-title": "Attach card",
"attach-card-hint": "Paste a card link (Copy card link on the other card), or type a card title on this board",
"attached-card-unavailable": "An attached card you cannot see",
"detach-card": "Remove attached card",
"attach-card-self": "A card cannot be attached to itself",
"attach-card-limit": "This card already has as many attached cards as it can have",
"attach-card-not-found": "No card you can see was found for that link",
"attach-card": "Joenem kad",
"attachCardPopup-title": "Joenem kad",
"attach-card-hint": "Pestem link blong wan kad (Kopiem link blong kad long narafala kad), o raetem nem blong wan kad long bod ia",
"attached-card-unavailable": "Wan kad we i joen we yu no save luk",
"detach-card": "Tekemaot kad we i joen",
"attach-card-self": "Yu no save joenem wan kad long hem wan",
"attach-card-limit": "Kad ia i gat finis olgeta kad we i save joen long hem",
"attach-card-not-found": "I no gat wan kad we yu save luk long link ia",
"auto-watch": "Lukluk ol bod otomatik taem oli mekem",
"avatar-too-big": "avatar i bigfala tumas (__size__ i maks)",
"back": "Gobak",
"board-change-color": "Jenisim kala",
"board-announcement": "Board announcement",
"board-announcement-enabled": "Show it to everyone on this board",
"board-announcement": "Toktok blong bod",
"board-announcement-enabled": "Soem long evriwan long bod ia",
"board-change-background-image": "Jenisim Pikja blong Baekgraon",
"board-background-image-url": "URL blong Pikja blong Baekgraon",
"add-background-image": "Adem Pikja blong Baekgraon",
@@ -284,7 +284,7 @@
"zoom-level": "Saes blong lukluk",
"enter-zoom-level": "Putum saes blong lukluk (50-300%):",
"board-view-cal": "Kalenda",
"board-view-calendar-mode": "Calendar Mode",
"board-view-calendar-mode": "Fasin blong kalenda",
"board-view-multiboard-cal": "Kalenda blong plante bod",
"board-view-swimlanes": "Ol rod",
"board-view-collapse": "Foldem",
@@ -686,32 +686,32 @@
"chooseBoardSourcePopup-title": "Karem i kam bod",
"createLabelPopup-title": "Mekem Mak",
"createCustomField": "Mekem Fil",
"custom-field-links": "Link fields to another card",
"custom-field-links-hint": "Custom fields with the same name and type on both cards are linked: when a value changes on one card, it is copied to the other card. Fields that only one card has are left alone.",
"custom-field-links-none": "This card's custom fields are not linked to any card.",
"custom-field-link-unavailable": "A linked card you cannot see",
"custom-field-link-fields": "Linked fields:",
"custom-field-link-no-fields": "No fields with the same name and type yet",
"custom-field-link-inactive": "Paused: the person who made this link can no longer edit both cards, or a card is archived.",
"custom-field-unlink": "Unlink",
"custom-field-link-target": "Card link (Copy card link), or search this board's cards",
"custom-field-link-direction": "Direction",
"custom-field-link-both": "Both ways: changes on either card go to the other",
"custom-field-link-send": "One way: changes on this card go to the main card",
"custom-field-link-sends": "Changes on this card go to that card",
"custom-field-link-receives": "Changes on that card come to this card",
"custom-field-link-add": "Link fields",
"field-link-self": "A card cannot be linked to itself.",
"field-link-limit": "This card, or that card, already has the most linked cards it can have.",
"field-link-archived": "An archived card cannot be linked.",
"field-link-invalid": "That is not a card link.",
"field-link-not-found": "Card not found.",
"field-link-not-allowed": "You need to be able to edit both cards.",
"cardCustomFieldLinksPopup-title": "Linked custom fields",
"custom-field-links": "Joenem ol fil long narafala kad",
"custom-field-links-hint": "Ol fil blong yu we i gat sem nem mo sem kaen long tufala kad i joen: taem wan valiu i jenis long wan kad, i kopi i go long narafala kad. Ol fil we wan kad nomo i gat, oli stap olsem.",
"custom-field-links-none": "Ol fil blong yu long kad ia oli no joen long eni kad.",
"custom-field-link-unavailable": "Wan kad we i joen we yu no save luk",
"custom-field-link-fields": "Ol fil we i joen:",
"custom-field-link-no-fields": "I no gat fil yet we i gat sem nem mo sem kaen",
"custom-field-link-inactive": "I stop smol: man we i mekem link ia i no save jenisim tufala kad bakegen, o wan kad i stap long akaev.",
"custom-field-unlink": "Brekem link",
"custom-field-link-target": "Link blong kad (Kopiem link blong kad), o lukaotem ol kad blong bod ia",
"custom-field-link-direction": "Rod we i go",
"custom-field-link-both": "Tu rod: ol jenis long eni kad i go long narafala",
"custom-field-link-send": "Wan rod: ol jenis long kad ia i go long bigfala kad",
"custom-field-link-sends": "Ol jenis long kad ia i go long kad ia we i stap longwe",
"custom-field-link-receives": "Ol jenis long narafala kad i kam long kad ia",
"custom-field-link-add": "Joenem ol fil",
"field-link-self": "Yu no save joenem wan kad long hem wan.",
"field-link-limit": "Kad ia, o narafala kad, i gat finis moa kad we i joen we i save gat.",
"field-link-archived": "Yu no save joenem wan kad we i stap long akaev.",
"field-link-invalid": "Hemia i no link blong kad.",
"field-link-not-found": "Kad i no stap.",
"field-link-not-allowed": "Yu mas gat raet blong jenisim tufala kad.",
"cardCustomFieldLinksPopup-title": "Ol fil blong yu we i joen",
"createCustomFieldPopup-title": "Mekem Fil",
"current": "naoia",
"custom-color": "Spesel kala",
"cards-use-list-color": "Cards without a colour of their own use this colour",
"cards-use-list-color": "Ol kad we oli no gat kala blong olgeta oli yusum kala ia",
"custom-field-delete-pop": "Yu no save tekem i kambak. Samting ia bae i tekemaot spesel fil ia long evri kad mo rablaotem histri blong hem.",
"custom-field-checkbox": "Bokis blong tikim",
"custom-field-currency": "Mane",
@@ -916,13 +916,13 @@
"import-board-instruction-focalboard": "Openem fael .boardarchive we i stap long zip mo putum insaed blong board.jsonl blong bod, wetem laen blong vesin o nogat. Propeti blong mekem ol grup long lukluk blong bod i kam ol lis, ol narafala propeti blong jusum oli kam ol mak, ol blok blong teks mo ol bokis blong tikim oli kam diskripsen mo lis blong jekem, mo ol komen oli stap yet. Ol pikja mo ol fael we oli joenem oli no kam insaed.",
"import-board-instruction-todoist": "Long Todoist, openem menyu blong projek mo jusum Export as a template, afta putum kopi blong CSV fael. Ol seksen oli kam ol lis, ol wok oli kam ol kad, ol smol wok oli kam wan lis blong jekem, ol not oli kam ol komen, mo @labels mo ol praoriti p1 kasem p3 oli kam ol lebel.",
"import-board-instruction-planner": "Long Microsoft Planner, openem menyu blong plan mo jusum Export plan to Excel, afta jusum fael .xlsx ia. Ol bakit oli kam ol lis mo ol wok oli kam ol kad, wetem ol man we oli givim wok long olgeta, ol deit, ol mak, jeklis mo diskripsen; Progress, Priority mo Completed By oli kam ol spesel fil.",
"import-board-instruction-meistertask": "In MeisterTask, click the project name, choose Export project and export the CSV, then paste it. Sections become lists and tasks become cards, with their notes, due date, assignee and tags; completed tasks keep their completion date. A CSV written for MeisterTask's own import works too.",
"import-board-instruction-obsidian": "In Obsidian, open the Kanban board's file as Markdown (or open the .md file in a text editor), then paste all of it. Lanes become lists with their card limits, cards keep their tags, dates, priority, fields and checklist, and the Archive becomes archived cards.",
"import-board-instruction-linear": "In Linear, export the issues as CSV (Settings, Import / Export, Export data, or a view's export), then paste the CSV. Statuses become lists, teams become swimlanes, and issues become cards with their assignee, labels, dates, parent issue, priority, estimate, project and cycle.",
"import-board-instruction-ticktick": "In TickTick, open Settings, Account, Backup & Import and generate a backup, then paste the CSV file. Each TickTick list becomes a swimlane and its columns become lists; tasks keep their tags, dates, checklist, priority and subtasks.",
"import-board-instruction-clickup": "In ClickUp, export the tasks as CSV (Settings, Imports / Exports, Export Items, or a List or Table view's export with all columns), then paste the CSV. Statuses become lists, ClickUp lists become swimlanes, and tasks become cards with their subtasks, assignees, tags, dates, priority and time.",
"import-board-instruction-nullboard": "In Nullboard, open the board, choose Export this board... from the menu, open the .nbx file in a text editor and paste its contents. Lists become lists and notes become cards: a note's first line is the card title and the rest its description. A raw note gets the label raw. A file with several boards imports its first board.",
"import-board-instruction-kanri": "In Kanri, open Import & Export and choose Export individual board (or Export all data), then paste the .json file. Columns become lists and cards become cards, with their description, due date, tags and colors; a card's tasks become a checklist. From an all-data export the first board is imported.",
"import-board-instruction-meistertask": "Long MeisterTask, klik long nem blong projek, jusum Export project mo sendem CSV i go, afta pestem. Ol seksen oli kam ol lis mo ol wok oli kam ol kad, wetem not, dedlaen, man we i gat wok mo ol tag blong olgeta; ol wok we i finis oli kipim deit we oli finis. Wan CSV we oli raetem blong MeisterTask i wok tu.",
"import-board-instruction-obsidian": "Long Obsidian, openem fael blong Kanban bod olsem Markdown (o openem fael .md long wan teks edita), afta pestem evri samting. Ol lein oli kam ol lis wetem limit blong kad, ol kad oli kipim ol tag, deit, praeoriti, fil mo jeklis blong olgeta, mo Archive i kam ol kad long akaev.",
"import-board-instruction-linear": "Long Linear, sendem ol isiu i go olsem CSV (Settings, Import / Export, Export data, o eksport blong wan viu), afta pestem CSV. Ol stetas oli kam ol lis, ol tim oli kam ol rod, mo ol isiu oli kam ol kad wetem man we i gat wok, ol mak, deit, papa isiu, praeoriti, estimet, projek mo saekol blong olgeta.",
"import-board-instruction-ticktick": "Long TickTick, openem Settings, Account, Backup & Import mo mekem wan bakap, afta pestem fael CSV. Evri lis blong TickTick i kam wan rod mo ol kolum blong hem oli kam ol lis; ol wok oli kipim ol tag, deit, jeklis, praeoriti mo smol wok blong olgeta.",
"import-board-instruction-clickup": "Long ClickUp, sendem ol wok i go olsem CSV (Settings, Imports / Exports, Export Items, o eksport blong wan List o Table viu wetem evri kolum), afta pestem CSV. Ol stetas oli kam ol lis, ol lis blong ClickUp oli kam ol rod, mo ol wok oli kam ol kad wetem smol wok, man we i gat wok, tag, deit, praeoriti mo taem blong olgeta.",
"import-board-instruction-nullboard": "Long Nullboard, openem bod, jusum Export this board... long menu, openem fael .nbx long wan teks edita mo pestem samting we i stap insaed. Ol lis oli kam ol lis mo ol not oli kam ol kad: fes laen blong wan not i nem blong kad mo narafala i stori blong hem. Wan not we i raw i kasem mak raw. Long wan fael we i gat plante bod, fes bod nomo i kam.",
"import-board-instruction-kanri": "Long Kanri, openem Import & Export mo jusum Export individual board (o Export all data), afta pestem fael .json. Ol kolum oli kam ol lis mo ol kad oli kam ol kad, wetem stori, dedlaen, tag mo kala blong olgeta; ol wok blong wan kad oli kam wan jeklis. Long wan eksport blong evri data, fes bod i kam.",
"import-board-instruction-planka": "Raetem adres blong Planka blong yu (olsem https://planka.example.com), nem blong yusa mo paswod blong yu long Planka, nao presem Soem Lis. Oli yusum olgeta blong karem bod nomo mo oli no kipim. Jusum wan bod mo presem Karem: WeKan i ridim long API blong Planka, wetem ol lis, kad, stori, dedlaen, mak, memba, lis blong wok mo toktok blong hem, nao gohed long tekem i kam olsem oltaem. Ol kad long stoa blong Planka oli kam olsem kad we oli stap long stoa; ol atajmen mo ol kad long rabis oli no kam.",
"import-board-instruction-mstodo": "Long Microsoft Graph Explorer (developer.microsoft.com/graph/graph-explorer), saen in, agri long tok-orait Tasks.Read mo kopi token long tab Access token. Pastem long ples ia mo kilik Soem Lis; token ia i blong karem ia nomo mo oli no kipim. Jusum evri lis o wan lis mo kilik Karem. Evri lis blong To Do i kam wan lis mo evri wok i kam wan kad, wetem not, dedlaen mo stat det, ol step olsem lis blong jekem, ol kategori olsem mak, mo bigfala impoten mo stejes olsem mak; ol wok we i finis oli go long stoa. Ol rimaenda mo ripit oli no kam.",
"import-board-instruction-kanbanflow": "Long KanbanFlow, openem menu blong bod, jusum Settings → API & Webhooks mo kopi API token blong bod. Pastem token long ples ia, kilik Soem Lis, jusum bod mo kilik Karem; token ia i blong karem ia nomo mo oli no kipim. Ol kolum oli kam lis mo ol rod oli stap rod; ol wok oli kipim stori, kala, mak, dedlaen, smol wok olsem lis blong jekem, taem we oli spenem, tingting blong taem, ol man mo toktok blong olgeta.",
@@ -941,19 +941,19 @@
"api-import-fetched": "I kam finis. Jekem ol memba daon mo tekem i kam.",
"api-import-failed": "Karem i no wok",
"api-import-nothing": "I no gat samting blong tekem i kam wetem akaon ia.",
"import-board-instruction-pivotal": "In Pivotal Tracker, open the project's MORE tab and choose Export CSV (or select stories and choose Bulk Actions, CSV), then paste the CSV. Each story state becomes a list and stories become cards, with their type and labels, owners, requester, dates, comments, tasks and blockers; Estimate becomes Story points and iterations become sprints.",
"import-board-instruction-tasksorg": "In Tasks.org, open Settings, Backups and choose Export tasks, then paste the contents of that .json file. Lists become lists and tasks become cards, with their notes, tags, dates, comments and subtasks; completed tasks keep their completion date, and priority becomes a custom field.",
"import-board-instruction-monday": "In monday.com, open the board menu, choose More actions and Export board to Excel (with updates and subitems if you like), then choose that .xlsx file. The Status column becomes lists and groups become swimlanes; people, dates, timeline and tags keep their places, other columns become custom fields and updates become comments.",
"import-board-instruction-superproductivity": "In Super Productivity, open Settings, then Sync & Backup, choose Export Data and paste the contents of the sp-backup .json file. Each project becomes a swimlane and tasks become cards in To Do, In Progress, Backlog or Done, with their notes, tags, due date, time spent and sub-tasks; archived tasks become archived cards.",
"import-board-instruction-taiga": "In Taiga, open Admin > Project > Export and download the project dump (unpack a .json.gz file first), then paste its JSON. User story statuses become lists, user stories become cards with their tasks as subtasks, epics and issues get swimlanes of their own, and sprints, tags, comments and custom attributes come along. Attachments are not imported.",
"import-board-instruction-vikunja": "In Vikunja, open Settings, choose Data Export and download the .zip file it sends you, then choose that file, or paste the data.json inside it. Kanban buckets become lists and tasks become cards, with their labels, assignees, dates, description, task lists as checklists and comments; priority, percent done and done become custom fields. Attachments are not imported.",
"import-board-instruction-quire": "In Quire, open the project menu and choose Export CSV, then paste the CSV. Statuses become lists and tasks become cards, with their subtasks as child cards, assignees, tags as labels, start, due and completed dates, description and priority as a custom field. Comments and attachments are not in Quire's CSV.",
"import-board-instruction-wrike": "In Wrike, export a project or folder to Excel, or fill in Wrike's Excel import template, then choose that .xlsx file. Folder and project rows become swimlanes, Status becomes lists and tasks become cards, with their Key, parent task, assignees, start and end dates, dependencies and description; Priority, Duration and other columns become custom fields.",
"import-board-instruction-teamwork": "In Teamwork.com, fill in the Excel task import template (Tasklist, Task, Description, Assign to, Start date, Due date, Priority, Estimated time, Tags, Status) and save it as .xlsx, then choose that file. Task lists become lists and tasks become cards; a task starting with -, # or > becomes a subtask of the task above it (--, ## or >> one level deeper). Assignees, dates and tags keep their places; Priority, Estimated time and Complete become custom fields.",
"import-board-instruction-businessmap": "In Businessmap (formerly Kanbanize), open Advanced Search, choose Configure results to pick the columns, download the Excel file, then choose that .xlsx file. Columns are read by their English names (Title, Column, Lane, Owner, Deadline ...): if your account uses another language, rename the header row to English first. Column becomes lists and Lane swimlanes; owners, tags, color, dates, comments and parent links keep their places, and Priority, Size, Type and other columns become custom fields.",
"import-board-instruction-redmine": "In Redmine, open the project's Issues, choose Also available in: CSV at the bottom of the list, pick the columns (All columns, with Description) and export, then paste the CSV. Redmine names the columns in your language, so set your language to English in My account before exporting. Each status becomes a list and issues become cards, with the tracker as a label, assignee, author, dates, parent task and related issues; priority, category, target version, estimated time, % Done and custom fields become custom fields.",
"import-board-instruction-notion": "In Notion, open the database as a full page, choose ••• and Export, pick Markdown & CSV, and choose the .zip file it downloads, or one database CSV from it, or paste that CSV. A column named Status becomes the lists (without one, the first column that looks like a select does, and the import report says which); rows become cards with their title, tags as labels, people as members, dates, and the page text as the description; other columns become custom fields. Relations, images and attachments are not imported.",
"import-board-instruction-plane": "In Plane, open Workspace Settings, choose Exports, export the projects as JSON, CSV or Excel and download the .zip file, then choose that file, or one file from inside it, or paste the JSON or CSV. States become lists, projects become swimlanes, and issues become cards with their identifier, parent, assignees, labels, dates, links and comments; priority, estimate, cycle and module become custom fields. Plane's export has no descriptions or attachments, so they are not imported.",
"import-board-instruction-pivotal": "Long Pivotal Tracker, openem tab MORE blong projek mo jusum Export CSV (o jusum ol stori mo jusum Bulk Actions, CSV), afta pestem CSV. Evri stet blong stori i kam wan lis mo ol stori oli kam ol kad, wetem kaen mo ol mak, ona, man we i askem, deit, toktok, wok mo ol samting we i blokem blong olgeta; Estimate i kam Story points mo ol iteresen oli kam ol sprint.",
"import-board-instruction-tasksorg": "Long Tasks.org, openem Settings, Backups mo jusum Export tasks, afta pestem samting we i stap insaed long fael .json ia. Ol lis oli kam ol lis mo ol wok oli kam ol kad, wetem not, tag, deit, toktok mo smol wok blong olgeta; ol wok we i finis oli kipim deit we oli finis, mo praeoriti i kam wan fil blong yu.",
"import-board-instruction-monday": "Long monday.com, openem menu blong bod, jusum More actions mo Export board to Excel (wetem ol apdet mo ol smol samting sapos yu wantem), afta jusum fael .xlsx ia. Kolum Status i kam ol lis mo ol grup oli kam ol rod; ol man, deit, taemlaen mo tag oli stap long ples blong olgeta, ol narafala kolum oli kam ol fil blong yu mo ol apdet oli kam ol toktok.",
"import-board-instruction-superproductivity": "Long Super Productivity, openem Settings, afta Sync & Backup, jusum Export Data mo pestem samting we i stap insaed long fael .json sp-backup. Evri projek i kam wan rod mo ol wok oli kam ol kad long To Do, In Progress, Backlog o Done, wetem not, tag, dedlaen, taem we oli yusum mo smol wok blong olgeta; ol wok long akaev oli kam ol kad long akaev.",
"import-board-instruction-taiga": "Long Taiga, openem Admin > Project > Export mo daonlodem damp blong projek (fes, openem fael .json.gz), afta pestem JSON blong hem. Ol stetas blong yusa stori oli kam ol lis, ol yusa stori oli kam ol kad wetem ol wok blong olgeta olsem smol wok, ol epik mo isiu oli kasem rod blong olgeta, mo ol sprint, tag, toktok mo ol atribiut blong yu oli kam tu. Ol atajmen oli no kam.",
"import-board-instruction-vikunja": "Long Vikunja, openem Settings, jusum Data Export mo daonlodem fael .zip we i sendem long yu, afta jusum fael ia, o pestem data.json we i stap insaed. Ol baket blong Kanban oli kam ol lis mo ol wok oli kam ol kad, wetem mak, man we i gat wok, deit, stori, lis blong wok olsem jeklis mo toktok blong olgeta; praeoriti, pesen we i finis mo finis oli kam ol fil blong yu. Ol atajmen oli no kam.",
"import-board-instruction-quire": "Long Quire, openem menu blong projek mo jusum Export CSV, afta pestem CSV. Ol stetas oli kam ol lis mo ol wok oli kam ol kad, wetem smol wok olsem pikinini kad, man we i gat wok, tag olsem mak, deit blong stat, dedlaen mo deit we i finis, stori mo praeoriti olsem wan fil blong yu. Ol toktok mo atajmen oli no stap long CSV blong Quire.",
"import-board-instruction-wrike": "Long Wrike, sendem wan projek o folda i go long Excel, o fulumap templet blong Excel import blong Wrike, afta jusum fael .xlsx ia. Ol laen blong folda mo projek oli kam ol rod, Status i kam ol lis mo ol wok oli kam ol kad, wetem Key, papa wok, man we i gat wok, deit blong stat mo en, ol samting we oli dipen long hem mo stori blong olgeta; Priority, Duration mo ol narafala kolum oli kam ol fil blong yu.",
"import-board-instruction-teamwork": "Long Teamwork.com, fulumap templet blong Excel wok import (Tasklist, Task, Description, Assign to, Start date, Due date, Priority, Estimated time, Tags, Status) mo sevem olsem .xlsx, afta jusum fael ia. Ol lis blong wok oli kam ol lis mo ol wok oli kam ol kad; wan wok we i stat wetem -, # o > i kam smol wok blong wok we i stap antap long hem (--, ## o >> wan level daon moa). Man we i gat wok, deit mo tag oli stap long ples blong olgeta; Priority, Estimated time mo Complete oli kam ol fil blong yu.",
"import-board-instruction-businessmap": "Long Businessmap (bifo Kanbanize), openem Advanced Search, jusum Configure results blong jusum ol kolum, daonlodem fael Excel, afta jusum fael .xlsx ia. Oli ridim ol kolum long nem blong olgeta long Inglis (Title, Column, Lane, Owner, Deadline ...): sapos akaon blong yu i yusum narafala lanwis, fes, jenisim nem blong laen blong hed i go long Inglis. Column i kam ol lis mo Lane i kam ol rod; ona, tag, kala, deit, toktok mo link blong papa oli stap long ples blong olgeta, mo Priority, Size, Type mo ol narafala kolum oli kam ol fil blong yu.",
"import-board-instruction-redmine": "Long Redmine, openem Issues blong projek, jusum Also available in: CSV long botom blong lis, jusum ol kolum (All columns, wetem Description) mo sendem i go, afta pestem CSV. Redmine i putum nem blong ol kolum long lanwis blong yu, ale jenisim lanwis blong yu i go long Inglis long My account bifo yu sendem i go. Evri stetas i kam wan lis mo ol isiu oli kam ol kad, wetem treka olsem mak, man we i gat wok, man we i raetem, deit, papa wok mo ol isiu we oli joen; praeoriti, kategori, vesen we oli wantem kasem, taem we oli tingbaot, % Done mo ol fil blong yu oli kam ol fil blong yu.",
"import-board-instruction-notion": "Long Notion, openem database olsem wan fulwan pej, jusum ••• mo Export, jusum Markdown & CSV, mo jusum fael .zip we i daonlod, o wan database CSV long hem, o pestem CSV ia. Wan kolum we nem blong hem Status i kam ol lis (sapos i no gat, fes kolum we i luk olsem wan jus i mekem, mo ripot blong import i talem wanem); ol laen oli kam ol kad wetem nem, tag olsem mak, ol man olsem memba, deit, mo teks blong pej olsem stori; ol narafala kolum oli kam ol fil blong yu. Ol rilesen, pija mo atajmen oli no kam.",
"import-board-instruction-plane": "Long Plane, openem Workspace Settings, jusum Exports, sendem ol projek i go olsem JSON, CSV o Excel mo daonlodem fael .zip, afta jusum fael ia, o wan fael insaed long hem, o pestem JSON o CSV. Ol stet oli kam ol lis, ol projek oli kam ol rod, mo ol isiu oli kam ol kad wetem namba, papa, man we i gat wok, mak, deit, link mo toktok blong olgeta; praeoriti, estimet, saekol mo modiul oli kam ol fil blong yu. Eksport blong Plane i no gat stori o atajmen, ale oli no kam.",
"import-board-instruction-trello": "Long bod blong Trello blong yu, go long 'Menu', afta 'More', 'Print and Export', 'Export JSON', mo kopiem teks we i kamaot.",
"import-board-instruction-csv": "Putum kopi blong ol valiu we koma i seraotem (CSV) / ol valiu we tab i seraotem (TSV).",
"import-board-instruction-jira": "Putum kopi blong JSON long lukaot blong isu blong Jira Cloud REST API (GET /rest/api/3/search/jql), o wan objek we i semmak olsem { \"issues\": [...] }. Ol stet blong Jira oli kam ol lis mo ol isu oli kam ol kad. Yu save adem wan array blong \"automationRules\" blong importem ol rul.",
@@ -962,33 +962,33 @@
"import-board-instruction-wekan": "Long bod blong yu, go long 'Menu', afta 'Export board', mo kopiem teks long fael we yu daonlodem.",
"import-board-instruction-about-errors": "Sapos yu kasem ol eroa taem yu karem i kam bod, samtaem karem i kam i wok yet, mo bod i stap long pej blong Olgeta Bod.",
"import-without-mapping-members": "Karem i kam we i no joenem ol memba (joenem biaen)",
"import-members-mode-heading": "People in the file:",
"import-members-mode-map": "Choose an existing user for each of them",
"import-members-mode-placeholder": "Keep them as placeholder users with their own names, to map to real users later",
"import-members-mode-me": "Make them all me",
"csv-mapping-title": "Which column is which?",
"csv-mapping-description": "Choose the column of the file that holds each card field. The choices were made from the column names; change any that are wrong, and choose (not in the file) for a field the file does not have.",
"csv-mapping-not-in-file": "(not in the file)",
"csv-mapping-column-number": "Column __number__",
"csv-mapping-list-missing": "The file has no column for the list. Choose one above, or type the name of the one list every card goes into.",
"csv-mapping-list-name": "List for every card",
"csv-mapping-custom-fields": "Other columns to import as text custom fields:",
"csv-mapping-boards": "Each sheet becomes its own board:",
"csv-mapping-skipped-sheets": "Sheets that are not imported:",
"csv-mapping-title-required": "Choose the column that holds the card titles.",
"csv-mapping-list-required": "Type the name of the list every card goes into, or choose the column that holds the list.",
"invalid-import-mapping": "The column mapping does not match the file.",
"import-many-boards": "Import many boards",
"import-many-boards-hint": "Choose several export files of this tool, or one .zip that holds them: each file becomes its own board, imported without member mapping. A .zip that is itself one export (Vikunja, Notion, Plane, or WeKan with attachments) is one board. When files are chosen here, Import imports them instead of the field above.",
"import-one-board-per-project": "One board per project",
"import-one-board-per-project-hint": "Each swimlane the import would make (a project, folder or list of the tool) becomes its own board. Parent cards and dependencies between those boards are listed in the import report.",
"import-many-progress": "Importing __done__ of __total__: __name__",
"import-many-results-heading": "Imported files",
"import-many-imported": "imported",
"export-all-boards": "Export all boards",
"export-selected-boards": "Export selected boards",
"export-all-boards-hint": "Every board you can export, in one download: Excel as one workbook with a sheet per board named after it, every other format as a .zip with a file per board.",
"exportAllBoardsPopup-title": "Export all boards",
"import-members-mode-heading": "Ol man long fael:",
"import-members-mode-map": "Jusum wan yusa we i stap finis blong evri wan long olgeta",
"import-members-mode-placeholder": "Kipim olgeta olsem ol yusa blong ples wetem nem blong olgeta, blong joenem wetem ol tru yusa biaen",
"import-members-mode-me": "Mekem olgeta evriwan i kam mi",
"csv-mapping-title": "Wanem kolum i wanem?",
"csv-mapping-description": "Jusum kolum blong fael we i holem evri fil blong kad. Oli mekem ol jus long nem blong ol kolum; jenisim eni we i rong, mo jusum (i no stap long fael) blong wan fil we fael i no gat.",
"csv-mapping-not-in-file": "(i no stap long fael)",
"csv-mapping-column-number": "Kolum __number__",
"csv-mapping-list-missing": "Fael i no gat kolum blong lis. Jusum wan antap, o raetem nem blong wan lis nomo we evri kad i go long hem.",
"csv-mapping-list-name": "Lis blong evri kad",
"csv-mapping-custom-fields": "Ol narafala kolum blong karem i kam olsem fil blong yu blong teks:",
"csv-mapping-boards": "Evri sit i kam bod blong hem wan:",
"csv-mapping-skipped-sheets": "Ol sit we oli no karem i kam:",
"csv-mapping-title-required": "Jusum kolum we i holem nem blong ol kad.",
"csv-mapping-list-required": "Raetem nem blong lis we evri kad i go long hem, o jusum kolum we i holem lis.",
"invalid-import-mapping": "Fasin we ol kolum oli joen i no stret wetem fael.",
"import-many-boards": "Tekem plante bod i kam",
"import-many-boards-hint": "Jusum plante fael blong eksport blong tul ia, o wan .zip we i holem olgeta: evri fael i kam bod blong hem wan, we i kam be i no joenem ol memba. Wan .zip we hem wan i wan eksport (Vikunja, Notion, Plane, o WeKan wetem atajmen) i wan bod nomo. Taem yu jusum ol fael long ples ia, Import i karem olgeta i kam, i no fil antap.",
"import-one-board-per-project": "Wan bod long evri projek",
"import-one-board-per-project-hint": "Evri rod we import bambae i mekem (wan projek, folda o lis blong tul) i kam bod blong hem wan. Ol papa kad mo ol samting we oli dipen long olgeta bitwin ol bod ia oli stap long ripot blong import.",
"import-many-progress": "I stap karem __done__ long __total__ i kam: __name__",
"import-many-results-heading": "Ol fael we oli kam finis",
"import-many-imported": "i kam finis",
"export-all-boards": "Sendem evri bod i go",
"export-selected-boards": "Sendem ol bod we yu jusum i go",
"export-all-boards-hint": "Evri bod we yu save sendem i go, long wan daonlod: Excel olsem wan buk wetem wan sit long evri bod we i karem nem blong hem, evri narafala fomat olsem wan .zip wetem wan fael long evri bod.",
"exportAllBoardsPopup-title": "Sendem evri bod i go",
"import-json-placeholder": "Putum stret data JSON blong yu long ples ia",
"import-csv-placeholder": "Putum stret data CSV/TSV blong yu long ples ia",
"import-json-file": "O jusum wan fael JSON blong Trello",
@@ -1026,17 +1026,17 @@
"trello-import-results": "Ol risal blong karem i kam",
"trello-api-credentials-required": "Plis raetem tufala ki mo token blong Trello API.",
"trello-api-credentials-saved": "Oli sevem ki mo token blong Trello API. Yu save soemaot ol wokples we yu no raetem olgeta bakegen.",
"exportToTrelloPopup-title": "Export to Trello",
"trello-export-desc": "Create a copy of this board in Trello through Trello's API, with your Trello API key and token. Swimlanes become labels named \"Swimlane: …\", which WeKan's Trello import turns back into swimlanes.",
"trello-export-include-attachments": "Include attachments",
"trello-export-start": "Export to Trello",
"trello-export-open-board": "Open the board in Trello",
"trello-export-warnings": "Not everything could be exported",
"sandstorm-trello-access-needed": "A Sandstorm grain has no Internet access of its own. Allow this grain to reach Trello first.",
"sandstorm-allow-trello-access": "Allow access to Trello",
"sandstorm-trello-access-granted": "This grain may reach Trello.",
"sandstorm-forget-access": "Forget",
"sandstorm-trello-save-label": "Trello, for importing and exporting boards",
"exportToTrelloPopup-title": "Sendem i go long Trello",
"trello-export-desc": "Mekem wan kopi blong bod ia long Trello tru long API blong Trello, wetem API ki mo token blong Trello blong yu. Ol rod oli kam ol mak we nem blong olgeta \"Swimlane: …\", we Trello import blong WeKan i tanem olgeta i kam bak ol rod.",
"trello-export-include-attachments": "Putum ol atajmen tu",
"trello-export-start": "Sendem i go long Trello",
"trello-export-open-board": "Openem bod long Trello",
"trello-export-warnings": "I no save sendem evri samting i go",
"sandstorm-trello-access-needed": "Wan Sandstorm grain i no gat Intanet blong hem wan. Fes, letem grain ia i go kasem Trello.",
"sandstorm-allow-trello-access": "Letem i go kasem Trello",
"sandstorm-trello-access-granted": "Grain ia i save go kasem Trello.",
"sandstorm-forget-access": "Fogetem",
"sandstorm-trello-save-label": "Trello, blong karem bod i kam mo sendem bod i go",
"trello-select-boards": "Plis jusum wan bod o moa blong karem i kam.",
"select-all": "Jusum evriwan",
"select-none": "No jusum wan samting",
@@ -1311,15 +1311,16 @@
"custom-login-logo-link-url": "URL blong Link blong Spesel Logo blong Go insaed",
"custom-help-link-url": "URL blong Spesel Link blong Help",
"text-below-custom-login-logo": "Raeting aninit long Spesel Logo blong Go insaed",
"login-background-color": "Kala blong baksaed blong pej blong login",
"automatic-linked-url-schemes": "Ol Spesel Fasin blong URL we yu save klik otomatik. Wan Fasin blong URL long wanwan laen",
"automatic-linked-url-schemes-hint": "Long fas seting, hem i emti, mekem se yu save klik long ol link blong web mo imel nomo. Evri kaen link long lis, olsem thunderlink o onenote, i openem program we oli rejistarem blong hem. javascript, data, vbscript mo ol kaen olsem oli neva kam ol link we yu save klik long olgeta.",
"external-link-pattern": "Otomatik link i go long wan aotsaed traka blong problem",
"external-link-pattern-description": "Hem i save mekem olsem \"#1234\" we oli stap long raeting blong kad i kam wan link i go long wan aotsaed traka blong problem. Livim wan spes nomo blong jampem samting ia.",
"external-link-pattern-prefix": "Prefiks blong tok",
"external-link-pattern-url": "Templet blong URL (yusum {number})",
"external-link-rules": "More link rules",
"external-link-rules-description": "One rule per line: the text to find, \" = \", and the address. Use {number} for digits and {identifier} for a word, in both, for example [{identifier}:{number}] = https://tracker.example.com/{identifier}/{number}.",
"external-link-identifier-aliases": "Abbreviations for {identifier}, for example TK=Task, IN=Incident",
"external-link-rules": "Moa rul blong link",
"external-link-rules-description": "Wan rul long wan laen: teks blong faenem, \" = \", mo adres. Yusum {number} blong namba mo {identifier} blong wan wod, long tufala, olsem [{identifier}:{number}] = https://tracker.example.com/{identifier}/{number}.",
"external-link-identifier-aliases": "Sotfala nem blong {identifier}, olsem TK=Task, IN=Incident",
"username": "Nem blong yusa",
"import-usernames": "Karem i kam ol Yusanem",
"view-it": "Lukluk hem",
@@ -1344,13 +1345,13 @@
"attachment-transfer-limits-title": "Ol Limit blong Saes blong Fael we i Joen mo Fael blong API",
"attachment-limits": "Ol limit",
"attachment-retention": "Taem blong kipim",
"attachment-retention-days": "Delete attachments after (days)",
"attachment-retention-days-help": "Like Delete on the card: the card history can restore them. Empty or 0: never.",
"attachment-retention-mode": "Count the days from",
"attachment-retention-purge": "Remove deleted attachments for good after (days)",
"attachment-retention-purge-help": "After this they can no longer be restored. Empty or 0: never.",
"attachment-retention-dry-run": "Dry run: only count, in the server log, change nothing",
"attachment-retention-max-per-run": "At most this many attachments per night",
"attachment-retention-days": "Diletem ol atajmen afta (dei)",
"attachment-retention-days-help": "Olsem Diletem long kad: histori blong kad i save tekem olgeta i kambak. Emti o 0: neva.",
"attachment-retention-mode": "Kaontem ol dei stat long",
"attachment-retention-purge": "Tekemaot olgeta ol atajmen we oli diletem finis afta (dei)",
"attachment-retention-purge-help": "Afta long hemia, yu no save tekem olgeta i kambak. Emti o 0: neva.",
"attachment-retention-dry-run": "Traem nomo: kaontem nomo, long log blong seva, no jenisim wan samting",
"attachment-retention-max-per-run": "Bigfala namba blong atajmen long wan naet",
"attachment-transfer-limits-description": "Setem ol limit blong saes blong fael we i bigfala olgeta blong aplodem mo daonlodem. Fasin blong API blong holem data i gat ol limit blong sefti blong hem long seva.",
"attachment-transfer-limits-saved": "Oli sevem ol limit blong sendem fael we i joen.",
"attachment-transfer-limits-save-failed": "I no save sevem ol limit blong sendem fael we i joen",
@@ -1400,51 +1401,51 @@
"error-notAuthorized": "Yu no gat raet blong lukluk pej ia.",
"webhook-title": "Nem blong Webhook",
"webhook-token": "Token (sapos yu wantem, blong Jekem hu nao)",
"webhook-payload-heading": "Webhook data",
"webhook-payload-description": "What outgoing webhooks (Rocket.Chat, Slack, ...) send. Default keeps the inherited setting; with nothing set, webhooks send what they always have.",
"webhook-payload-text": "Send the message text",
"webhook-payload-fields": "Send these fields",
"webhook-payload-field-standard": "Standard fields (WEBHOOKS_ATTRIBUTES)",
"webhook-payload-field-ids": "IDs",
"webhook-payload-field-names": "Names and titles",
"webhook-payload-field-links": "Links to the card and board",
"webhook-payload-field-actor": "Who did it",
"webhook-payload-field-people": "The member or assignee it is about",
"webhook-payload-field-details": "Details: comment, description, values and dates",
"webhook-hide-identity": "Leave my name out of outgoing webhooks",
"webhook-someone": "A member",
"webhook-payload-heading": "Data blong webhook",
"webhook-payload-description": "Wanem ol webhook we i go aot (Rocket.Chat, Slack, ...) oli sendem. Defolt i kipim seting we i kam long antap; sapos i no gat seting, ol webhook oli sendem samting we oli stap sendem oltaem.",
"webhook-payload-text": "Sendem teks blong mesej",
"webhook-payload-fields": "Sendem ol fil ia",
"webhook-payload-field-standard": "Ol standet fil (WEBHOOKS_ATTRIBUTES)",
"webhook-payload-field-ids": "Ol ID",
"webhook-payload-field-names": "Ol nem mo taetel",
"webhook-payload-field-links": "Ol link i go long kad mo bod",
"webhook-payload-field-actor": "Hu i mekem",
"webhook-payload-field-people": "Memba o man we i gat wok we samting ia i tokbaot",
"webhook-payload-field-details": "Ol smol samting: toktok, stori, valiu mo deit",
"webhook-hide-identity": "No putum nem blong mi long ol webhook we i go aot",
"webhook-someone": "Wan memba",
"notification-delivery-layout": "Fasin blong putum ol samting",
"notification-delivery-layout-classic": "One sentence (classic)",
"notification-delivery-layout-clear": "Clearly arranged: board, who, card in bold",
"notification-delivery-parts": "Show",
"notification-delivery-part-board": "Board name and link",
"notification-delivery-part-actor": "Who did it",
"notification-delivery-part-card": "Card title and link",
"notification-delivery-layout-classic": "Wan sentens (olfala fasin)",
"notification-delivery-layout-clear": "Klia: bod, hu, kad long strong raeting",
"notification-delivery-parts": "Soem",
"notification-delivery-part-board": "Nem mo link blong bod",
"notification-delivery-part-actor": "Hu i mekem",
"notification-delivery-part-card": "Nem mo link blong kad",
"notification-delivery-part-list": "Lis",
"notification-delivery-part-swimlane": "Rod",
"notification-delivery-part-details": "Comment or changed value",
"notification-delivery-part-dates": "Dates",
"notification-delivery-part-link": "Link on its own line",
"notification-delivery-grouping": "Grouping",
"notification-delivery-grouping-all": "Everything due in one message",
"notification-delivery-grouping-board": "One per board",
"notification-delivery-grouping-card": "One per card",
"notification-delivery-grouping-none": "Each on its own",
"notification-delivery-schedule": "When to send",
"notification-delivery-schedule-immediate": "Immediately",
"notification-delivery-schedule-interval": "Every few minutes or hours",
"notification-delivery-schedule-daily": "Once a day",
"notification-delivery-interval": "Every",
"notification-delivery-minutes": "__count__ minutes",
"notification-delivery-part-details": "Toktok o valiu we i jenis",
"notification-delivery-part-dates": "Ol deit",
"notification-delivery-part-link": "Link long laen blong hem wan",
"notification-delivery-grouping": "Putum long grup",
"notification-delivery-grouping-all": "Evri samting long wan mesej",
"notification-delivery-grouping-board": "Wan long evri bod",
"notification-delivery-grouping-card": "Wan long evri kad",
"notification-delivery-grouping-none": "Evri wan i stap hem wan",
"notification-delivery-schedule": "Wetaem blong sendem",
"notification-delivery-schedule-immediate": "Stret nao",
"notification-delivery-schedule-interval": "Evri smol minit o aoa",
"notification-delivery-schedule-daily": "Wan taem long wan dei",
"notification-delivery-interval": "Evri",
"notification-delivery-minutes": "__count__ minit",
"notification-delivery-daily-time": "Long",
"notification-delivery-quiet": "Quiet hours (wait until they end)",
"notification-delivery-quiet": "Ol aoa blong kwaet (wet gogo oli finis)",
"notification-delivery-quiet-from": "Stat long",
"notification-delivery-quiet-to": "Go kasem",
"notification-delivery-quiet-clear": "No quiet hours",
"notification-delivery-timezone": "Time zone:",
"notification-delivery-invalid": "The setting could not be saved.",
"notification-group-card": "__count__ notifications on card __card__",
"notification-group-board": "__count__ notifications on board __board__",
"notification-delivery-quiet-clear": "I no gat aoa blong kwaet",
"notification-delivery-timezone": "Taem son:",
"notification-delivery-invalid": "I no save sevem seting.",
"notification-group-card": "__count__ niusem long kad __card__",
"notification-group-board": "__count__ niusem long bod __board__",
"outgoing-webhooks": "Ol webhook we oli go aot",
"bidirectional-webhooks": "Ol webhook blong tufala rod",
"outgoingWebhooksPopup-title": "Ol webhook we oli go aot",
@@ -1499,7 +1500,7 @@
"showLabel-field-on-card": "Soemaot mak blong fil long smol kad",
"showSum-field-on-list": "Soemaot totel blong ol fil long antap blong lis",
"admin-only-field": "Admin blong bod nomo i save luk",
"read-only-field": "Read-only: every member sees the value, only board admins change it",
"read-only-field": "Blong ridim nomo: evri memba i luk valiu, ol admin blong bod nomo oli jenisim",
"yes": "Io",
"no": "Nogat",
"accounts": "Ol akaon",
@@ -1691,11 +1692,11 @@
"r-format-auto": "Faenemaot otomatik",
"r-import-workflow-note": "Karem i kam wan pikja blong rod blong wok we oli karem i go long n8n o Node-RED. Ol triga/aksen we oli save oli joen wetem ol rul blong WeKan; ol wan we oli no joen bae oli talem.",
"r-import-unmapped": "I no save joenem __count__ laen",
"r-wrike-workflow": "Wrike workflow",
"r-wrike-workflow-note": "A Wrike workflow is a list of statuses, each in the Active, Completed, Deferred or Cancelled group. Export writes this board's lists as one, named after the board, in the JSON Wrike's API returns for workflows: create that workflow in Wrike before importing the board's Wrike Excel export, so its statuses are applied. Import reads that JSON pasted above, from WeKan or from Wrike's GET /workflows, into the chosen board: a status without a list becomes a list, and a rule marks a card complete when it is moved into a Completed or Cancelled status and incomplete in an Active or Deferred one. Wrike's own automation rules cannot be exported from Wrike.",
"r-export-wrike-workflow": "Export Wrike workflow",
"r-import-wrike-workflow": "Import Wrike workflow",
"r-import-wrike-workflow-done": "Wrike workflow \"__name__\": __lists__ list(s) and __rules__ rule(s) added, __skipped__ part(s) not read",
"r-wrike-workflow": "Rod blong wok blong Wrike",
"r-wrike-workflow-note": "Wan rod blong wok blong Wrike i wan lis blong ol stetas, evri wan i stap long grup Active, Completed, Deferred o Cancelled. Eksport i raetem ol lis blong bod ia olsem wan, we i karem nem blong bod, long JSON we API blong Wrike i givim blong ol rod blong wok: mekem rod blong wok ia long Wrike bifo yu karem Wrike Excel eksport blong bod i kam, blong oli yusum ol stetas blong hem. Import i ridim JSON ia we yu pestem antap, we i kam long WeKan o long GET /workflows blong Wrike, i go long bod we yu jusum: wan stetas we i no gat lis i kam wan lis, mo wan rul i makem wan kad se i finis taem oli muvum i go long wan stetas Completed o Cancelled, mo se i no finis long Active o Deferred. Yu no save sendem ol rul blong otomesen blong Wrike i kamaot long Wrike.",
"r-export-wrike-workflow": "Sendem rod blong wok blong Wrike i go",
"r-import-wrike-workflow": "Tekem rod blong wok blong Wrike i kam",
"r-import-wrike-workflow-done": "Rod blong wok blong Wrike \"__name__\": __lists__ lis mo __rules__ rul oli kam, __skipped__ pat oli no ridim",
"r-set-scheduled-triggers": "Ol triga we oli setem taem blong olgeta",
"r-set-button-triggers": "Ol baten",
"r-when-scheduled": "Folem wan taem we oli setem",
@@ -1751,8 +1752,8 @@
"set-filter": "Setem filta",
"r-moved-to": "Movem finis long",
"r-moved-from": "Movem finis long",
"r-moved-forward": "moved forward to a later list",
"r-moved-back": "moved back to an earlier list",
"r-moved-forward": "i muv i go fored long wan lis we i kam biaen",
"r-moved-back": "i muv i go bak long wan lis we i kam fastaem",
"r-archived": "Movem finis long Akaev",
"r-unarchived": "Kamaot long Akaev",
"r-commented": "oli raetem toktok long hem",
@@ -1800,8 +1801,8 @@
"r-add-actinguser-member": "Adem yusa we i statem rul ia olsem memba",
"r-remove-all": "Tekemaot evri memba long kad",
"r-assignee": "asaeni",
"r-add-actinguser-assignee": "Add the user who triggered this rule as an assignee",
"r-remove-all-assignees": "Remove all assignees from the card",
"r-add-actinguser-assignee": "Putum yusa we i statem rul ia olsem man we i gat wok",
"r-remove-all-assignees": "Tekemaot evri man we i gat wok long kad",
"r-remove-all-labels": "Tekemaot evri mak long kad",
"r-set-color": "Setem kala long",
"r-checklist": "jeklis",
@@ -1893,7 +1894,7 @@
"oauth-provider-secret-set": "Wan sikret i stap",
"oauth-providers-login-style": "Stael blong login",
"oauth-providers-merge-existing-users": "Joenem wetem wan akaon we i stap finis mo i gat sem imel",
"oauth-providers-allowed-email-domains": "Only these email domains may sign in with these providers (comma separated; empty allows all)",
"oauth-providers-allowed-email-domains": "Ol imel domen ia nomo oli save saen in wetem ol provaeda ia (koma i seraotem; emti i letem evriwan)",
"oauth-account-conflict": "Wan akaon wetem imel ia i stap finis mo oli no mekem hem wetem login rod ia",
"sign-in-with": "Login wetem %s",
"login-origin-mismatch": "Yu no save finisim go insaed tru long wan narafala seves long adres ya. Oli setem WeKan blong __expected__, be pej ya i open long __actual__. Openem WeKan long __expected__, o askem admin blong setem ROOT_URL i go long __actual__.",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Taem taetol blong wan niu kad i stat wetem [Tekis] mo i gat moa tekis biaen, Tekis i kam wan mak blong kad (oli mekem sapos bod i no gat mak wetem nem ia) mo oli tekemaot long taetol. Of: taetol i stap olsem we i stap.",
"keep-add-card-form-open": "Livim fom blong Adem kad i stap open afta we yu adem wan kad",
"keep-add-card-form-open-description": "On: afta we yu adem wan kad, fom i stap open wetem wan taetel we i emti blong nekis kad. Of: fom i satem stret taem we kad i kam insaed.",
"show-focus-outline": "Soem aot-lain blong fokas",
"checklist-ding-sound": "Plei wan saon taem wan samting long jeklis i finis",
"checklist-ding-sound-description": "Plei wan sotfala saon taem yu makem wan samting long jeklis. Long stat, hem i of.",
"assignee": "Man we i gat wok",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Koneksen i no wok",
"cloud-settings-saved": "Oli sevem ol seting blong storej",
"cloud-settings-save-failed": "I no save sevem ol seting blong storej",
"cloud": "Klaod",
"cloud-settings-desc": "Wetem WeKan long Weddy, Cloud Panel i givim plante WeKan domen long wan seva nomo, evri wan i gat data blong hem. Long Cloud Panel Domen, SuperAdmin i lukaotem ol domen mo CloudFlare.",
"cloud-panel-domain": "Cloud Panel Domen",
"cloud-superadmin-username": "Nem blong yusa blong SuperAdmin",
"cloud-saved": "Oli sevem finis.",
"cloud-panel-not-allowed": "Cloud Panel i blong SuperAdmin nomo, long Cloud Panel Domen.",
"cloud-customer-name": "Nem blong kastoma",
"cloud-customer-email": "Adres blong imel blong kastoma",
"cloud-billing-email": "Adres blong imel blong bil",
"cloud-start-date": "Dei blong stat",
"cloud-end-date": "Dei blong finis",
"cloudflare-api-key": "Ki blong CloudFlare API",
"cloudflare-api-key-desc": "Wetem wan ki blong CloudFlare API, wan domen we zon blong hem i stap long akaon blong yu long CloudFlare i kasem wan DNS rekod we i pas long proxy mo wan setifiket blong CloudFlare Origin. Ol narafala domen oli kasem wan setifiket blong Let's Encrypt. Ki ia i stap long seva mo bambae oli no soem bakegen.",
"cloudflare-api-key-set": "Wan ki blong CloudFlare API i stap.",
"cloudflare-api-key-not-set": "I no gat ki blong CloudFlare API: evri domen i kasem wan setifiket blong Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Movem fael we i joen i go long Azure Blob Storage",
@@ -3274,7 +3291,7 @@
"import-report-open-board": "Openem bod",
"import-parts-instruction": "Ol pat we oli tikim nomo bae oli karem i kam. Sem jois i wok taem yu karem i go.",
"import-wekan-file": "Karem i kam long wan fael .json o .zip we oli karem i go:",
"import-file-label": "Choose the export file, or paste its text below:",
"import-file-label": "Jusum fael blong eksport, o pestem teks blong hem daon long hia:",
"sum-of-number-fields": "Sam blong ol namba kastom fil we oli soem antap long lis",
"date-range-of-fields": "Taem bitwin ol deit long ol spesel fil we oli makem blong soemaot long antap blong lis",
"more-options": "Moa jois",
@@ -3301,9 +3318,9 @@
"wip-limit-group-select-swimlane": "Jusum Rod",
"wip-limit-group-apply-swimlane": "Yusum long swimlane",
"subtask-inherit-parent-labels": "Tekem ol mak blong stamba kad",
"subtask-mark-done": "Mark subtask as done",
"subtask-mark-not-done": "Mark subtask as not done",
"subtask-done-no-permission": "You cannot change whether this subtask is done",
"subtask-mark-done": "Makem smol wok se i finis",
"subtask-mark-not-done": "Makem smol wok se i no finis",
"subtask-done-no-permission": "Yu no save jenisim sapos smol wok ia i finis o no",
"chart-forecast-none-remaining": "I no gat samting moa blong wokem - evri kad i finis.",
"chart-forecast-no-velocity": "__remaining__ kad i stap open yet; i no gat wan we i jas finis blong tokbaot deit.",
"chart-forecast-projected": "Long spid blong resen taem blong __average__ kad/wik, __remaining__ kad we oli stap open yet i sapos blong finis long __date__.",
@@ -4107,8 +4124,8 @@
"scrum-new-sprint": "Niufala sprint",
"scrum-releases": "Ol rilisim mo ol niufala haf blong prodak",
"scrum-release": "Rilisim",
"scrum-release-scope": "Cards in this release",
"scrum-releases-select-help": "A card can be in several releases. Hold Ctrl (Cmd on a Mac) to choose more than one; clear them all to remove the card from every release.",
"scrum-release-scope": "Ol kad long rilis ia",
"scrum-releases-select-help": "Wan kad i save stap long plante rilis. Holem Ctrl (Cmd long Mac) blong jusum moa long wan; klinim evriwan blong tekemaot kad long evri rilis.",
"scrum-select-sprint": "Jusum wan sprint blong luk ripot blong hem.",
"scrum-backlog": "Lis blong wok we i stap wet",
"scrum-backlog-help": "Putum wok long oda blong lis blong wok we i stap wet. Putum ol kad long wan sprint we oli planem o we i aktif.",
@@ -4178,22 +4195,22 @@
"scrum-import-resume": "Finisim wok blong tekem i kam",
"scrum-import-discard": "Kanselem wok blong tekem i kam we i no finis",
"scrum-import-recovery-hint": "Wok blong tekem i kam we i stop taem sistem i stat bakegen, yu save finisim long ples ia taem i stop finis. Sipos oli no sevem ful plan blong hem, yu save kanselem nomo; hemia i no jenisim wan samting long bod.",
"scrum-import-into-board": "Import Scrum planning into this board",
"scrum-import-into-board-hint": "Choose a WeKan board export or Scrum transfer file. Sprints and releases this board already has are linked, never duplicated. Cards are matched by ID, or by card number and title; cards that do not match are listed and left unchanged. Preview first to see what will change.",
"scrum-import-into-board": "Tekem plan blong Scrum i kam long bod ia",
"scrum-import-into-board-hint": "Jusum wan eksport blong WeKan bod o wan fael blong Scrum. Ol sprint mo rilis we bod ia i gat finis oli joen, oli neva kam tu taem. Oli faenem ol kad long ID, o long namba mo nem blong kad; ol kad we oli no stret oli stap long lis mo oli no jenis. Fes, lukluk bifo blong luk wanem bambae i jenis.",
"scrum-import-preview": "Luk fastaem",
"scrum-import-choose-file": "Choose a file first.",
"scrum-import-invalid-file": "This file is not valid JSON.",
"scrum-import-preview-sprints": "Sprints: __created__ new, __matched__ already on this board, __skipped__ not imported",
"scrum-import-preview-releases": "Releases: __created__ new, __matched__ already on this board, __skipped__ not imported",
"scrum-import-preview-cards": "Cards: __updated__ to update, __unchanged__ unchanged, __unmatched__ not matched",
"scrum-import-preview-nothing": "Nothing to change: this board already has this planning.",
"scrum-import-into-board-done": "The Scrum planning was imported and recorded in History.",
"scrum-import-card-not-matched": "A card was not found on this board: __reference__",
"scrum-import-card-ambiguous": "A card matches more than one card on this board: __reference__",
"scrum-import-card-on-another-board": "A card belongs to another board and was left unchanged: __reference__",
"scrum-import-record-ambiguous": "A sprint or release matches more than one on this board: __reference__",
"scrum-import-record-not-imported": "A sprint, release or event could not be imported: __reference__",
"scrum-import-sprint-finished": "A card was not moved into a finished sprint: __reference__",
"scrum-import-choose-file": "Fes, jusum wan fael.",
"scrum-import-invalid-file": "Fael ia i no stret JSON.",
"scrum-import-preview-sprints": "Ol sprint: __created__ niu, __matched__ oli stap finis long bod ia, __skipped__ oli no kam",
"scrum-import-preview-releases": "Ol rilis: __created__ niu, __matched__ oli stap finis long bod ia, __skipped__ oli no kam",
"scrum-import-preview-cards": "Ol kad: __updated__ blong apdetem, __unchanged__ oli no jenis, __unmatched__ oli no faenem",
"scrum-import-preview-nothing": "I no gat samting blong jenisim: bod ia i gat plan ia finis.",
"scrum-import-into-board-done": "Plan blong Scrum i kam finis mo i stap long Histori.",
"scrum-import-card-not-matched": "Wan kad i no stap long bod ia: __reference__",
"scrum-import-card-ambiguous": "Wan kad i stret wetem moa long wan kad long bod ia: __reference__",
"scrum-import-card-on-another-board": "Wan kad i blong narafala bod mo i no jenis: __reference__",
"scrum-import-record-ambiguous": "Wan sprint o rilis i stret wetem moa long wan long bod ia: __reference__",
"scrum-import-record-not-imported": "I no save karem wan sprint, rilis o samting we i hapen i kam: __reference__",
"scrum-import-sprint-finished": "Wan kad i no muv i go long wan sprint we i finis: __reference__",
"sync-conflict-heading": "Stretem ol konflik blong sinkronaes",
"sync-conflict-hint": "Jusum wan valiu blong wanwan konflik. Sinkronaes i traem bakegen afta wanwan jus. I no gat wan samting we oli sendem i go long sistem blong sos.",
"sync-conflict-local": "Valiu blong WeKan naoia",
@@ -4294,10 +4311,10 @@
"sync-original-time": "Fas estimet blong taem (aoa)",
"sync-remaining-time": "Estimet blong taem we i stap yet (aoa)",
"sync-time-estimate-hint": "Ol estimet blong taem oli yusum ol fil blong namba blong fas taem/taem we i stap yet we import blong Jira i mekem. Wan fil nomo we i semmak i mas stap long bod ia. Oli lukluk long ol lokol jenis olsem ol konflik; oli ignorim ol valiu we oli no stap long sos mo wan null we oli talem klia i kliarem valiu.",
"sync-planning-sprint": "Sprint (Scrum planning)",
"sync-planning-releases": "Releases (Scrum planning)",
"sync-planning-fields": "Sprint and releases",
"sync-planning-hint": "Sprint and releases are synced only while Scrum is enabled on this board. A sprint or release that is missing is created on this board, with the dates the source gives; an existing one is matched by its source ID first, then by name. A local planning change stays until the source changes that issue's planning, and a first Sync never removes planning.",
"sync-planning-sprint": "Sprint (plan blong Scrum)",
"sync-planning-releases": "Ol rilis (plan blong Scrum)",
"sync-planning-fields": "Sprint mo ol rilis",
"sync-planning-hint": "Sprint mo ol rilis oli sinkem nomo taem Scrum i on long bod ia. Wan sprint o rilis we i no stap, oli mekem long bod ia, wetem ol deit we sos i givim; wan we i stap finis, fes oli faenem long ID blong sos, afta long nem. Wan jenis long plan long ples ia i stap gogo sos i jenisim plan blong isiu ia, mo fes Sink i neva tekemaot plan.",
"activity-recovery-heading": "Ol notifikasen blong aktiviti we oli stap wet",
"activity-recovery-description": "Oli traem ol notifikasen we oli no finis bakegen otomatik. Lukluk long ol aktiviti we oli no stap o we oli jenis bifo yu traem bakegen. Traem bakegen i neva mekem wan aktiviti bakegen.",
"activity-recovery-empty": "I no gat notifikasen blong aktiviti we i stap wet.",
@@ -4378,53 +4395,53 @@
"rule-email-legacy-attempt-exists": "Oli sanem imel ya finis, traem sanem finis, o sakemaot finis.",
"rule-email-legacy-not-legacy": "Oli linkim imel ya bakegen finis.",
"rule-email-legacy-failed": "Oli no save finisim wok ya. Lodem pej bakegen mo traem bakegen.",
"stuck-sync-operation-heading": "List Sync operations that cannot be replayed",
"stuck-sync-operation-description": "Each of these saved List Sync operations stopped part way and can no longer be finished, because its list was removed, recreated or reconfigured, or the user who started it lost write access. While one is pending, that list cannot Sync. Discard it to let the list Sync again: changes already applied stay, the remaining saved changes are never written, and the next Sync compares the list with its source again.",
"stuck-sync-operation-list": "Board / list",
"stuck-sync-operation-heading": "Ol wok blong Lis Sink we i no save mekem bakegen",
"stuck-sync-operation-description": "Evri wan long ol wok blong Lis Sink ia we oli sevem i stop long medel mo i no save finis bakegen, from lis blong hem oli tekemaot, mekem bakegen o jenisim seting, o yusa we i statem i lusum raet blong raet. Taem wan i stap wet, lis ia i no save Sink. Sakemaot blong letem lis i Sink bakegen: ol jenis we i hapen finis oli stap, ol narafala jenis we oli sevem oli neva raet, mo nekis Sink i lukluk lis wetem sos blong hem bakegen.",
"stuck-sync-operation-list": "Bod / lis",
"stuck-sync-operation-progress": "Wok we i gohed",
"stuck-sync-operation-reason": "Risen",
"stuck-sync-operation-applied": "__applied__ of __total__ changes applied",
"stuck-sync-operation-reason-scope-changed": "The list was removed, recreated or its Sync settings changed",
"stuck-sync-operation-reason-access-denied": "The user who started it no longer has write access to the whole list",
"stuck-sync-operation-reason-trigger-unknown": "Saved by an earlier WeKan version without what started it",
"stuck-sync-operation-reason-intent-missing": "The record of who started it is missing",
"stuck-sync-operation-reason-unknown": "Could not be checked now",
"stuck-sync-operation-replayable-now": "It can be replayed again now and will finish by itself, so it cannot be discarded.",
"stuck-sync-operation-applied": "__applied__ long __total__ jenis oli hapen",
"stuck-sync-operation-reason-scope-changed": "Oli tekemaot lis, mekem bakegen, o seting blong Sink blong hem i jenis",
"stuck-sync-operation-reason-access-denied": "Yusa we i statem i no moa gat raet blong raet long fulwan lis",
"stuck-sync-operation-reason-trigger-unknown": "Wan olfala vesen blong WeKan i sevem we i no gat samting we i statem",
"stuck-sync-operation-reason-intent-missing": "Rekod blong hu i statem i no stap",
"stuck-sync-operation-reason-unknown": "I no save jekem naoia",
"stuck-sync-operation-replayable-now": "I save mekem bakegen naoia mo bambae i finis hem wan, ale yu no save sakemaot.",
"stuck-sync-operation-discard": "Sakemaot",
"stuck-sync-operation-discard-confirm": "Discard this saved List Sync operation? Changes it already applied stay; the rest are never written. The list can then Sync again.",
"stuck-sync-operation-discard-confirm": "Sakemaot wok blong Lis Sink ia we oli sevem? Ol jenis we i mekem finis oli stap; ol narafala oli neva raet. Afta lis i save Sink bakegen.",
"stuck-sync-operation-refresh": "Rifres",
"stuck-sync-operation-empty": "No List Sync operations are waiting for review.",
"stuck-sync-operation-truncated": "Only the oldest 50 are shown. Discard some and refresh to see more.",
"stuck-sync-operation-unavailable": "Could not load the List Sync operations. Refresh to try again.",
"stuck-sync-operation-missing": "This operation is no longer pending.",
"stuck-sync-operation-not-stuck": "This operation has not been found stuck, so it cannot be discarded.",
"stuck-sync-operation-replayable": "This operation can be replayed again now and will finish by itself, so it was not discarded.",
"stuck-sync-operation-busy": "This list is syncing right now. Try again in a moment.",
"stuck-sync-operation-empty": "I no gat wok blong Lis Sink we i stap wet blong lukluk.",
"stuck-sync-operation-truncated": "50 we oli moa olfala nomo oli soem. Sakemaot sam mo rifres blong luk moa.",
"stuck-sync-operation-unavailable": "I no save lodem ol wok blong Lis Sink. Rifres blong traem bakegen.",
"stuck-sync-operation-missing": "Wok ia i no moa stap wet.",
"stuck-sync-operation-not-stuck": "Oli no faenem se wok ia i stak, ale yu no save sakemaot.",
"stuck-sync-operation-replayable": "Wok ia i save mekem bakegen naoia mo bambae i finis hem wan, ale oli no sakemaot.",
"stuck-sync-operation-busy": "Lis ia i stap sink stret naoia. Traem bakegen smol taem afta.",
"stuck-sync-operation-failed": "Oli no save finisim wok ya. Lodem pej bakegen mo traem bakegen.",
"interrupted-import-heading": "Board imports that stopped before finishing",
"interrupted-import-description": "Each of these board imports stopped part way - the server restarted, or the import failed - and left a partial board. An import cannot be continued, because its source file is not kept. Keep the board as it is, or discard it and import the file again. Discarding removes the board this import created and everything on it, including anything added to it since.",
"interrupted-import-heading": "Ol import blong bod we oli stop bifo oli finis",
"interrupted-import-description": "Evri wan long ol import blong bod ia i stop long medel - seva i stat bakegen, o import i no wok - mo i livim wan bod we i no fulwan. Yu no save gohed wetem wan import, from oli no kipim fael blong hem. Kipim bod olsem we i stap, o sakemaot mo karem fael i kam bakegen. Sakemaot i tekemaot bod we import ia i mekem mo evri samting long hem, wetem eni samting we oli putum afta.",
"interrupted-import-board": "Bod",
"interrupted-import-progress": "Where it stopped",
"interrupted-import-created": "Created so far",
"interrupted-import-source": "Imported from __source__",
"interrupted-import-state-stopped": "Stopped without finishing, at __stage__",
"interrupted-import-state-failed": "Failed with __error__, at __stage__",
"interrupted-import-state-discarding": "Being discarded; discard again to finish",
"interrupted-import-scrum": "Its Scrum stage has its own checkpoint (__state__, __next__ of __total__ steps). If you keep the board, finish or discard it from the board's Scrum import recovery.",
"interrupted-import-counts": "__swimlanes__ swimlanes, __lists__ lists, __cards__ cards, __checklists__ checklists, __comments__ comments, __attachments__ attachments",
"interrupted-import-no-board": "No board was created",
"interrupted-import-keep": "Keep",
"interrupted-import-progress": "Wea i stop",
"interrupted-import-created": "Samting we i mekem kasem naoia",
"interrupted-import-source": "I kam long __source__",
"interrupted-import-state-stopped": "I stop be i no finis, long __stage__",
"interrupted-import-state-failed": "I no wok wetem __error__, long __stage__",
"interrupted-import-state-discarding": "I stap sakemaot; sakemaot bakegen blong finisim",
"interrupted-import-scrum": "Stej blong Scrum blong hem i gat jekpoen blong hem wan (__state__, __next__ long __total__ step). Sapos yu kipim bod, finisim o sakemaot long Scrum import rekavri blong bod.",
"interrupted-import-counts": "__swimlanes__ rod, __lists__ lis, __cards__ kad, __checklists__ jeklis, __comments__ toktok, __attachments__ atajmen",
"interrupted-import-no-board": "I no mekem wan bod",
"interrupted-import-keep": "Kipim",
"interrupted-import-discard": "Sakemaot",
"interrupted-import-keep-confirm": "Keep this partial board as it is? Nothing is removed, and the import is no longer listed here.",
"interrupted-import-discard-confirm": "Discard this import? The board it created and everything on it are removed permanently.",
"interrupted-import-keep-confirm": "Kipim bod ia we i no fulwan olsem we i stap? I no tekemaot wan samting, mo import i no moa stap long lis ia.",
"interrupted-import-discard-confirm": "Sakemaot import ia? Bod we i mekem mo evri samting long hem bambae oli lus olgeta.",
"interrupted-import-refresh": "Rifres",
"interrupted-import-empty": "No interrupted board imports are waiting for review.",
"interrupted-import-truncated": "Only the oldest 50 are shown. Keep or discard some and refresh to see more.",
"interrupted-import-unavailable": "Could not load the interrupted board imports. Refresh to try again.",
"interrupted-import-missing": "This import is no longer recorded.",
"interrupted-import-not-interrupted": "This import is not waiting for a decision any more.",
"interrupted-import-foreign-board": "The board with this id was not created by this import, so it was not touched.",
"interrupted-import-scrum-busy": "Its Scrum stage is still being written or recovered. Try again in a few minutes.",
"interrupted-import-empty": "I no gat import blong bod we i stop we i stap wet blong lukluk.",
"interrupted-import-truncated": "50 we oli moa olfala nomo oli soem. Kipim o sakemaot sam mo rifres blong luk moa.",
"interrupted-import-unavailable": "I no save lodem ol import blong bod we oli stop. Rifres blong traem bakegen.",
"interrupted-import-missing": "Import ia i no moa stap long rekod.",
"interrupted-import-not-interrupted": "Import ia i no moa wet blong wan disisen.",
"interrupted-import-foreign-board": "Import ia i no mekem bod wetem id ia, ale i no tajem.",
"interrupted-import-scrum-busy": "Stej blong Scrum blong hem i stap raet o rekava yet. Traem bakegen long sam minit.",
"interrupted-import-failed": "Oli no save finisim wok ya. Lodem pej bakegen mo traem bakegen.",
"saml-login-not-started": "Yu no bin statem SAML login ia long tab ia blong brausa. Plis go insaed bakegen.",
"move-selection-before": "Bifo",
@@ -4434,13 +4451,13 @@
"history-request-hint": "Taem yu traem bakegen, hem i ripitim semfala rikwest nomo, mekem se hem i no save tekemaot wan nara jenis.",
"history-request-retry": "Traem bakegen",
"history-request-forget": "Fogetem",
"scrum-history-checkpoint-stuck": "An undo, redo or restore of Scrum changes stopped on a conflict. Scrum edits on this board are blocked until it is resolved.",
"scrum-history-checkpoint-counts": "__applied__ of __total__ changes were written, __pending__ were not, and __conflicted__ were changed by someone else since.",
"scrum-history-checkpoint-hint": "Rolling back puts every changed record back as it was before; it is offered only when nobody else has changed those records since. Keeping the board as it is removes the stopped operation and changes no records. Either way, it is recorded in Admin Panel, Problems, Recovery.",
"scrum-history-checkpoint-rollback": "Roll back",
"scrum-history-checkpoint-discard": "Keep the board as it is",
"scrum-history-checkpoint-discard-confirm": "Keep every record as it is now, including the changes the stopped operation already wrote?",
"scrum-history-checkpoint-ask-admin": "A board administrator can roll it back or keep the board as it is.",
"scrum-history-checkpoint-stuck": "Wan andu, ridu o ristoa blong ol jenis blong Scrum i stop long wan rao. Ol jenis blong Scrum long bod ia oli blok gogo oli stretem.",
"scrum-history-checkpoint-counts": "__applied__ long __total__ jenis oli raet, __pending__ oli no raet, mo __conflicted__ narafala man i jenisim afta.",
"scrum-history-checkpoint-hint": "Tanem i go bak i putum evri rekod we i jenis olsem we i stap bifo; i stap nomo taem i no gat narafala man we i jenisim ol rekod ia afta. Kipim bod olsem we i stap i tekemaot wok we i stop mo i no jenisim eni rekod. Wanem rod yu jusum, i stap long Panel blong admin, Ol problem, Rekavri.",
"scrum-history-checkpoint-rollback": "Tanem i go bak",
"scrum-history-checkpoint-discard": "Kipim bod olsem we i stap",
"scrum-history-checkpoint-discard-confirm": "Kipim evri rekod olsem we i stap naoia, wetem ol jenis we wok we i stop i raetem finis?",
"scrum-history-checkpoint-ask-admin": "Wan admin blong bod i save tanem i go bak o kipim bod olsem we i stap.",
"header-login": "Go insaed tru long ol heda blong HTTP",
"login-setting-clear-secret": "Tekemaot valiu we i stap long panel blong admin",
"login-setting-after-restart": "I stat wok afta we WeKan i stat bakegen",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Donni logo lasigiden URL kɛrɛnkɛrɛnnen",
"custom-help-link-url": "Dɛmɛ lasigiden URL kɛrɛnkɛrɛnnen",
"text-below-custom-login-logo": "Sɛbɛnni donni logo kɛrɛnkɛrɛnnen duguma",
"login-background-color": "Kuntilenni gafe jukɔrɔ kulɛri",
"automatic-linked-url-schemes": "URL cogoya kɛrɛnkɛrɛnnen minnu ka kan ka digi yɛrɛ ma. URL cogoya kelen sira kelen kelen bɛɛ la",
"automatic-linked-url-schemes-hint": "A daminɛ na, a bɛ to kolon, o la i bɛ se ka web ani imɛli siriliw dɔrɔn digi. Sɛbɛnni kɔnɔ sikiima kelen-kelen bɛɛ, misali la thunderlink walima onenote, bɛ porogaramu min tɔgɔ sɛbɛnna a kama, o dayɛlɛ. javascript, data, vbscript ani sikiimaw minnu bɛ i nʼa fɔ olu, olu tɛ kɛ siriliw ye abada.",
"external-link-pattern": "Sɛbɛnni ka baara kɛ ni kunbɛnjuru kunkan ka gafe ye kɔfɛ",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Ni karti kura kunnafoni kuntigi daminɛna ni [Sɛbɛn] ye ani sɛbɛn wɛrɛ b'a kɔfɛ, Sɛbɛn bɛ kɛ karti in taamasiyɛn ye (a bɛ dila ni walan tɛ taamasiyɛn si ye o tɔgɔ la) ani a bɛ bɔ kunnafoni kuntigi la. A dabilalen: kunnafoni kuntigi bɛ to i n'a fɔ a bɛ cogo min na.",
"keep-add-card-form-open": "Karti fara a kan fɔrimu to a dayɛlɛlen na karti farali kɔfɛ",
"keep-add-card-form-open-description": "A bɛ baara la: ni karti farala a kan, fɔrimu bɛ to a dayɛlɛlen na ni tɔgɔ lankolon ye karti nata kama. A tɛ baara la: fɔrimu bɛ datugu ni karti farala a kan dɔrɔn.",
"show-focus-outline": "Fokisi lamini jira",
"checklist-ding-sound": "Kanfa mankan tuma min na checklist fɛn dafara",
"checklist-ding-sound-description": "Kanfa mankan surunman tuma min na i bɛ checklist fɛn sɛbɛn. A tɛ baara la ka bɔ a bɔyɔrɔ la.",
"assignee": "Baarakɛla",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Ɲɔgɔnya ma ɲɛ",
"cloud-settings-saved": "Mara yɔrɔ labɛnniw maralen don",
"cloud-settings-save-failed": "Mara yɔrɔ labɛnniw marali ma ɲɛ",
"cloud": "Sankaba",
"cloud-settings-desc": "Ni WeKan ye Weddy kɔnɔ, Cloud Panel bɛ WeKan domɛni caman baara ka bɔ server kelen na, u kelen-kelen bɛɛ ni a yɛrɛ ka kunnafoniw. Cloud Panel Domɛni kan, SuperAdmin bɛ domɛniw ni CloudFlare ɲɛmɔgɔya.",
"cloud-panel-domain": "Cloud Panel Domɛni",
"cloud-superadmin-username": "SuperAdmin baarakɛla tɔgɔ",
"cloud-saved": "A marala.",
"cloud-panel-not-allowed": "Cloud Panel ye SuperAdmin dama ta ye, Cloud Panel Domɛni kan.",
"cloud-customer-name": "Sannikɛla tɔgɔ",
"cloud-customer-email": "Sannikɛla imɛli ladɛrɛsi",
"cloud-billing-email": "Sarali imɛli ladɛrɛsi",
"cloud-start-date": "Daminɛ don",
"cloud-end-date": "Laban don",
"cloudflare-api-key": "CloudFlare API kilé",
"cloudflare-api-key-desc": "Ni CloudFlare API kilé ye, domɛni min ka zone bɛ i ka CloudFlare jatebɔ la, o bɛ proxy DNS sɛbɛn ni CloudFlare Origin sɛbɛn sɔrɔ. Domɛni tɔw bɛ Let's Encrypt sɛbɛn sɔrɔ. Kilé bɛ mara server kan, a tɛna jira tugun abada.",
"cloudflare-api-key-set": "CloudFlare API kilé marala.",
"cloudflare-api-key-not-set": "CloudFlare API kilé si ma mara: domɛni bɛɛ bɛ Let's Encrypt sɛbɛn sɔrɔ.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Ɲɔgɔnnan bila Azure Blob Storage la",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "কাস্টম প্রবেশ লোগো লিংকের URL",
"custom-help-link-url": "কাস্টম সহায়তা লিংকের URL",
"text-below-custom-login-logo": "কাস্টম প্রবেশ লোগোর নিচের লেখা",
"login-background-color": "লগইন পৃষ্ঠার পটভূমির রং",
"automatic-linked-url-schemes": "স্বয়ংক্রিয়ভাবে ক্লিকযোগ্য কাস্টম URL স্কিম। প্রতি লাইনে একটি URL স্কিম",
"automatic-linked-url-schemes-hint": "ডিফল্টভাবে ফাঁকা, তাই শুধু ওয়েব ও মেইল লিংক ক্লিক করা যায়। তালিকাভুক্ত প্রতিটি স্কিম, যেমন thunderlink বা onenote, তার জন্য নিবন্ধিত অ্যাপ খোলে। javascript, data, vbscript ও একই ধরনের স্কিম কখনো লিংক করা হয় না।",
"external-link-pattern": "বহিরাগত ইস্যু ট্র্যাকারে স্বয়ংক্রিয় লিঙ্ক",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "নতুন কার্ডের শিরোনাম [লেখা] দিয়ে শুরু হয়ে তারপর আরও লেখা থাকলে, লেখা কার্ডটির লেবেল হয়ে যায় (বোর্ডে সেই নামের লেবেল না থাকলে তৈরি করা হয়) এবং শিরোনাম থেকে সরানো হয়। বন্ধ: শিরোনাম যেমন আছে তেমনই থাকে।",
"keep-add-card-form-open": "কার্ড যোগ করার পরে কার্ড যোগ করুন ফর্মটি খোলা রাখুন",
"keep-add-card-form-open-description": "চালু: কার্ড যোগ করার পরে, পরের কার্ডের জন্য খালি শিরোনাম সহ ফর্মটি খোলা থাকে। বন্ধ: কার্ড যোগ হওয়ার সাথে সাথে ফর্মটি বন্ধ হয়ে যায়।",
"show-focus-outline": "ফোকাস আউটলাইন দেখান",
"checklist-ding-sound": "চেকলিস্ট আইটেম সম্পূর্ণ হলে শব্দ বাজান",
"checklist-ding-sound-description": "চেকলিস্ট আইটেম চেক করার সময় সংক্ষিপ্ত শব্দ বাজান। ডিফল্টভাবে বন্ধ।",
"assignee": "দায়িত্বপ্রাপ্ত",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "সংযোগ ব্যর্থ",
"cloud-settings-saved": "সংরক্ষণাগার সেটিংস সংরক্ষিত হয়েছে",
"cloud-settings-save-failed": "সংরক্ষণাগার সেটিংস সংরক্ষণ ব্যর্থ হয়েছে",
"cloud": "ক্লাউড",
"cloud-settings-desc": "Weddy-তে WeKan দিয়ে Cloud Panel একটি সার্ভার থেকে অনেক WeKan ডোমেইন পরিবেশন করে, প্রতিটির নিজস্ব ডেটা থাকে। Cloud Panel ডোমেইনে SuperAdmin ডোমেইনগুলো এবং CloudFlare পরিচালনা করেন।",
"cloud-panel-domain": "Cloud Panel ডোমেইন",
"cloud-superadmin-username": "SuperAdmin ব্যবহারকারীর নাম",
"cloud-saved": "সংরক্ষিত হয়েছে।",
"cloud-panel-not-allowed": "Cloud Panel কেবল SuperAdmin-এর জন্য, Cloud Panel ডোমেইনে।",
"cloud-customer-name": "গ্রাহকের নাম",
"cloud-customer-email": "গ্রাহকের ইমেইল ঠিকানা",
"cloud-billing-email": "বিলিং ইমেইল ঠিকানা",
"cloud-start-date": "শুরুর তারিখ",
"cloud-end-date": "শেষের তারিখ",
"cloudflare-api-key": "CloudFlare API কী",
"cloudflare-api-key-desc": "CloudFlare API কী থাকলে, যে ডোমেইনের জোন আপনার CloudFlare অ্যাকাউন্টে আছে সেটি একটি প্রক্সি করা DNS রেকর্ড এবং একটি CloudFlare Origin সার্টিফিকেট পায়। অন্যান্য ডোমেইন একটি Let's Encrypt সার্টিফিকেট পায়। কী-টি সার্ভারে সংরক্ষিত থাকে এবং আর কখনো দেখানো হয় না।",
"cloudflare-api-key-set": "একটি CloudFlare API কী সংরক্ষিত আছে।",
"cloudflare-api-key-not-set": "কোনো CloudFlare API কী সংরক্ষিত নেই: প্রতিটি ডোমেইন একটি Let's Encrypt সার্টিফিকেট পায়।",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "সংযুক্তি Azure Blob Storage-এ সরান",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "རང་བཟོས་ཐོ་འཇུག་རྟགས་རིས་མཐུད་ཐག་ URL",
"custom-help-link-url": "རང་བཟོས་རོགས་རམ་མཐུད་ཐག་ URL",
"text-below-custom-login-logo": "རང་བཟོས་ཐོ་འཇུག་རྟགས་རིས་འོག་གི་ཡི་གེ",
"login-background-color": "ནང་འཛུལ་ཤོག་ངོས་ཀྱི་རྒྱབ་ལྗོངས་ཚོན་མདོག",
"automatic-linked-url-schemes": "རང་འགུལ་གྱིས་མནན་ཐུབ་པའི་རང་བཟོས་ URL འཆར་གཞི། ཐིག་རེར་ URL འཆར་གཞི་གཅིག",
"automatic-linked-url-schemes-hint": "སྔོན་སྒྲིག་ལྟར་སྟོང་པ་ཡིན་པས། དྲ་རྒྱ་དང་གློག་འཕྲིན་གྱི་དྲ་མཐུད་ཁོ་ན་ལ་མནན་ཆོག ཐོ་གཞུང་ནང་བཀོད་པའི་འཆར་གཞི་རེ་རེས། དཔེར་ན་ thunderlink ཡང་ན་ onenote ལྟ་བུས། དེའི་ཆེད་དུ་ཐོ་འགོད་བྱས་པའི་ཉེར་སྤྱོད་ཁ་ཕྱེ་བ་ཡིན། javascript དང་ data དང་ vbscript དང་དེ་དག་དང་འདྲ་བའི་འཆར་གཞི་རྣམས་ནམ་ཡང་དྲ་མཐུད་དུ་མི་བསྒྱུར།",
"external-link-pattern": "ཕྱིའི་དཀའ་ངལ་རྗེས་འདེད་ལུ་རང་བཞིན་མཐུད་ལམ།",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "ཤོག་བྱང་གསར་པའི་མཚན་བྱང་[ཡི་གེ]ནས་འགོ་ཚུགས་ཏེ་རྗེས་སུ་ཡི་གེ་གཞན་ཡོད་ན། ཡི་གེ་དེ་ཤོག་བྱང་གི་ཁ་བྱང་དུ་འགྱུར། (བྱང་བུ་ལ་མིང་དེའི་ཁ་བྱང་མེད་ན་གསར་བཟོ་བྱེད།) མཚན་བྱང་ནས་བསུབ། བཀག་ན། མཚན་བྱང་ཇི་བཞིན་ཉར།",
"keep-add-card-form-open": "ཤོག་བྱང་སྣོན་རྗེས་ཤོག་བྱང་སྣོན་པའི་རེའུ་མིག་ཁ་ཕྱེ་སྟེ་བཞག",
"keep-add-card-form-open-description": "ཁ་ཕྱེ་: ཤོག་བྱང་སྣོན་རྗེས་རེའུ་མིག་དེ་ཤོག་བྱང་རྗེས་མའི་ཆེད་དུ་ཁ་བྱང་སྟོང་པ་དང་བཅས་ཁ་ཕྱེ་སྟེ་སྡོད། ཁ་རྒྱག་: ཤོག་བྱང་སྣོན་མ་ཐག་རེའུ་མིག་ཁ་རྒྱག་གི་རེད།",
"show-focus-outline": "ཆེད་སྦྱོར་གྱི་མཐའ་ཐིག་སྟོན།",
"checklist-ding-sound": "ཞིབ་བཤེར་ཐོ་གཞུང་གི་རྣམ་གྲངས་ཚང་བའི་སྐབས་སྒྲ་སྐད་གཏོང་།",
"checklist-ding-sound-description": "ཞིབ་བཤེར་ཐོ་གཞུང་གི་རྣམ་གྲངས་ཞིག་ལ་རྟགས་བཀོད་སྐབས་སྒྲ་སྐད་ཐུང་ངུ་ཞིག་གཏོང་། སྔོན་སྒྲིག་ནི་ཁ་རྒྱག་པ་ཡིན།",
"assignee": "འགན་འཁུར་བ།",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "སྦྲེལ་མཐུད་ཕམ་པ།",
"cloud-settings-saved": "ཉར་ཚགས་སྒྲིག་ཉར་ཟིན།",
"cloud-settings-save-failed": "ཉར་ཚགས་སྒྲིག་ཉར་མ་ཐུབ།",
"cloud": "སྤྲིན།",
"cloud-settings-desc": "Weddy ནང་གི་ WeKan དང་མཉམ་དུ། Cloud Panel གྱིས་ཞབས་ཞུ་འཕྲུལ་ཆས་གཅིག་ནས་ WeKan ཁྱབ་ཁོངས་མང་པོར་ཞབས་ཞུ་སྒྲུབ། རེ་རེར་རང་གི་གཞི་གྲངས་ཡོད། Cloud Panel ཁྱབ་ཁོངས་སུ་ SuperAdmin གྱིས་ཁྱབ་ཁོངས་དང་ CloudFlare དོ་དམ་བྱེད།",
"cloud-panel-domain": "Cloud Panel ཁྱབ་ཁོངས།",
"cloud-superadmin-username": "SuperAdmin བེད་སྤྱོད་མཁན་གྱི་མིང་།",
"cloud-saved": "ཉར་ཚགས་བྱས་ཟིན།",
"cloud-panel-not-allowed": "Cloud Panel ནི་ SuperAdmin ཁོ་ན་ལ་ཡིན། Cloud Panel ཁྱབ་ཁོངས་སུ།",
"cloud-customer-name": "མཁོ་མཁན་གྱི་མིང་།",
"cloud-customer-email": "མཁོ་མཁན་གྱི་གློག་འཕྲིན་ཁ་བྱང་།",
"cloud-billing-email": "འཇལ་ཐོའི་གློག་འཕྲིན་ཁ་བྱང་།",
"cloud-start-date": "འགོ་འཛུགས་ཚེས་གྲངས།",
"cloud-end-date": "མཇུག་སྒྲིལ་ཚེས་གྲངས།",
"cloudflare-api-key": "CloudFlare API ལྡེ་མིག",
"cloudflare-api-key-desc": "CloudFlare API ལྡེ་མིག་ཡོད་ན། ཁྱེད་ཀྱི་ CloudFlare རྩིས་ཐོའི་ནང་ zone ཡོད་པའི་ཁྱབ་ཁོངས་ལ་ proxy བྱས་པའི་ DNS ཟིན་ཐོ་དང་ CloudFlare Origin ལག་ཁྱེར་ཐོབ། ཁྱབ་ཁོངས་གཞན་ལ་ Let's Encrypt ལག་ཁྱེར་ཐོབ། ལྡེ་མིག་ཞབས་ཞུ་འཕྲུལ་ཆས་སུ་ཉར་ཞིང་ཡང་བསྐྱར་སྟོན་མི་སྲིད།",
"cloudflare-api-key-set": "CloudFlare API ལྡེ་མིག་ཉར་ཟིན།",
"cloudflare-api-key-not-set": "CloudFlare API ལྡེ་མིག་གང་ཡང་ཉར་མེད། ཁྱབ་ཁོངས་རེ་རེར་ Let's Encrypt ལག་ཁྱེར་ཐོབ།",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "ཟུར་སྣོན་ Azure Blob Storage ལ་སྤོ།",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL liamm al logo kennaskañ personelaet",
"custom-help-link-url": "URL al liamm skoazell personelaet",
"text-below-custom-login-logo": "Testenn dindan al logo kennaskañ personelaet",
"login-background-color": "Liv drekleur ar bajenn kevreañ",
"automatic-linked-url-schemes": "Steuñvioù URL personelaet a rank bezañ klikadus ent emgefre. Ur steuñv URL dre linenn",
"automatic-linked-url-schemes-hint": "Goullo dre ziouer, setu al liammoù web ha postel hepken zo klikadus. Pep skouerenn listennet, da skouer thunderlink pe onenote, a zigor an arload enrollet eviti. Ne vez ket krouet liammoù evit javascript, data, vbscript pe skouerennoù heñvel morse.",
"external-link-pattern": "Liamm emgefre ouzh un heuliad kudennoù diavaez",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Pa grog titl ur gartenn nevez gant [Testenn] heuliet gant muioc'h a destenn, e teu Testenn da vezañ un dikedenn eus ar gartenn (krouet ma n'eus hini ebet gant an anv-se en daolenn) hag e vez lamet eus an titl. Lazhet: miret e vez an titl evel m'emañ.",
"keep-add-card-form-open": "Derc'hel digor ar furmskrid Ouzhpennañ ur gartenn goude bezañ ouzhpennet ur gartenn",
"keep-add-card-form-open-description": "Enaouet: goude ouzhpennañ ur gartenn e chom digor ar furmskrid gant un titl goullo evit ar gartenn war-lerc'h. Lazhet: serret e vez ar furmskrid kerkent ha ma vez ouzhpennet ar gartenn.",
"show-focus-outline": "Diskouez trolinenn ar fokus",
"checklist-ding-sound": "Seniñ ur son pa vez echuet un elfenn eus roll gwiriañ",
"checklist-ding-sound-description": "Seniñ ur son berr pa vez merket un elfenn eus roll gwiriañ. Diweredekaet dre ziouer.",
"assignee": "Den deverket",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "C'hwitet eo ar c'hennask",
"cloud-settings-saved": "Enrollet eo arventennoù ar c'hadaviñ",
"cloud-settings-save-failed": "C'hwitet eo enrollañ arventennoù ar c'hadaviñ",
"cloud": "Koumoul",
"cloud-settings-desc": "Gant WeKan e Weddy e servij ar Cloud Panel kalz domanioù WeKan diwar ur servijer hepken, pep hini gant e roadennoù dezhañ. War domani ar Cloud Panel e vera ar SuperAdmin an domanioù ha CloudFlare.",
"cloud-panel-domain": "Domani ar Cloud Panel",
"cloud-superadmin-username": "Anv implijer ar SuperAdmin",
"cloud-saved": "Enrollet.",
"cloud-panel-not-allowed": "Evit ar SuperAdmin hepken eo ar Cloud Panel, war domani ar Cloud Panel.",
"cloud-customer-name": "Anv ar pratik",
"cloud-customer-email": "Chomlec'h postel ar pratik",
"cloud-billing-email": "Chomlec'h postel ar fakturennoù",
"cloud-start-date": "Deiziad kregiñ",
"cloud-end-date": "Deiziad echuiñ",
"cloudflare-api-key": "Alc'hwez API CloudFlare",
"cloudflare-api-key-desc": "Gant un alc'hwez API CloudFlare e resev un domani e zonenn en ho kont CloudFlare un enrolladenn DNS dre proksi hag un testeni CloudFlare Origin. An domanioù all a resev un testeni Let's Encrypt. Enrollet eo an alc'hwez war ar servijer ha ne vo morse diskouezet en-dro.",
"cloudflare-api-key-set": "Enrollet eo un alc'hwez API CloudFlare.",
"cloudflare-api-key-not-set": "N'eus alc'hwez API CloudFlare enrollet ebet: pep domani a resev un testeni Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Dilec'hiañ ar stagadenn da Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL poveznice prilagođenog logotipa za prijavu",
"custom-help-link-url": "URL prilagođene poveznice za pomoć",
"text-below-custom-login-logo": "Tekst ispod prilagođenog logotipa za prijavu",
"login-background-color": "Boja pozadine stranice za prijavu",
"automatic-linked-url-schemes": "Prilagođene URL sheme koje automatski trebaju biti poveznice na koje se može kliknuti. Jedna URL shema po redu",
"automatic-linked-url-schemes-hint": "Podrazumijevano je prazno, pa je moguće kliknuti samo web-veze i veze e-pošte. Svaka navedena shema, naprimjer thunderlink ili onenote, otvara aplikaciju registrovanu za nju. Za sheme javascript, data, vbscript i slične nikada se ne prave veze.",
"external-link-pattern": "Automatsko povezivanje s vanjskim praćenjem problema",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Kada naslov nove kartice počinje sa [Tekst] nakon čega slijedi još teksta, Tekst postaje oznaka kartice (kreira se ako tabla nema oznaku s tim imenom) i uklanja se iz naslova. Isključeno: naslov ostaje kakav jeste.",
"keep-add-card-form-open": "Zadrži obrazac za dodavanje kartice otvorenim nakon dodavanja kartice",
"keep-add-card-form-open-description": "Uključeno: nakon dodavanja kartice obrazac ostaje otvoren s praznim naslovom za sljedeću karticu. Isključeno: obrazac se zatvara čim se kartica doda.",
"show-focus-outline": "Prikaži okvir fokusa",
"checklist-ding-sound": "Reproduciraj zvuk kada se stavka kontrolne liste dovrši",
"checklist-ding-sound-description": "Reproduciraj kratak zvuk kada označite stavku kontrolne liste. Podrazumevano isključeno.",
"assignee": "Zaduženi",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Veza nije uspjela",
"cloud-settings-saved": "Postavke pohranjivanja spremljene",
"cloud-settings-save-failed": "Spremanje postavki pohranjivanja nije uspjelo",
"cloud": "Oblak",
"cloud-settings-desc": "Uz WeKan u Weddyju, Cloud Panel poslužuje mnogo WeKan domena s jednog servera, svaki sa svojim podacima. Na domeni Cloud Panela, SuperAdmin upravlja domenama i CloudFlareom.",
"cloud-panel-domain": "Domena Cloud Panela",
"cloud-superadmin-username": "Korisničko ime SuperAdmina",
"cloud-saved": "Sačuvano.",
"cloud-panel-not-allowed": "Cloud Panel je samo za SuperAdmina, na domeni Cloud Panela.",
"cloud-customer-name": "Ime kupca",
"cloud-customer-email": "Adresa e-pošte kupca",
"cloud-billing-email": "Adresa e-pošte za naplatu",
"cloud-start-date": "Datum početka",
"cloud-end-date": "Datum završetka",
"cloudflare-api-key": "CloudFlare API ključ",
"cloudflare-api-key-desc": "S CloudFlare API ključem, domena čija je zona u vašem CloudFlare računu dobija DNS zapis preko proxyja i CloudFlare Origin certifikat. Ostale domene dobijaju Let's Encrypt certifikat. Ključ se sprema na serveru i nikada se više ne prikazuje.",
"cloudflare-api-key-set": "CloudFlare API ključ je spremljen.",
"cloudflare-api-key-not-set": "Nijedan CloudFlare API ključ nije spremljen: svaka domena dobija Let's Encrypt certifikat.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Premjesti privitak u Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "Нэбтэрэлгын тусхай логотипой холбооһоной URL",
"custom-help-link-url": "Тусхай туһаламжын холбооһоной URL",
"text-below-custom-login-logo": "Нэбтэрэлгын тусхай логотипой доорохи бэшэг",
"login-background-color": "Нэбтэрэhэн хуудаһанай арьяа үнгэ",
"automatic-linked-url-schemes": "Автоматаар дарагдадаг болохо тусхай URL схемэнүүд. Мүр бүхэндэ нэгэ URL схемэ",
"automatic-linked-url-schemes-hint": "Анхандаа хооһон, тиимэһээ гансал веб болон сахим захидалай холбооһонууд дээрэ даража болохо. Жагсаалтада оруулагдаһан схемэ бүхэн, жэшээнь thunderlink гү, али onenote, тэрээндэ бүридхэгдэһэн программые нээнэ. javascript, data, vbscript болон эдээндэ адли схемэнүүд хэзээдэшье холбооһон болгогдохогүй.",
"external-link-pattern": "Гадаадын асуудалай хиналтада автоматаар холбоо",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Шэнэ карточкын гаршаг [Бэшэг] гэжэ эхилээд, саашаа бэшэгтэй байбал, Бэшэг карточкын тэмдэг болоно (самбарта тиимэ нэрэтэй тэмдэг үгы һаа, бүтээгдэнэ) ба гаршагһаа хасагдана. Унтараагдаһан: гаршаг байһан шэгээрээ үлэнэ.",
"keep-add-card-form-open": "Карточко нэмэһэнэй һүүлдэ «Карточко нэмэхэ» формые нээлтэтэй орхихо",
"keep-add-card-form-open-description": "Асаалтатай: карточко нэмэгдэһэнэй һүүлдэ форма дараагай карточкын хооһон гарчагтай нээлтэтэй үлэдэг. Унтараалтатай: карточко нэмэгдэнгүүт форма хаагдана.",
"show-focus-outline": "Фокусай хүрээ харуулха",
"checklist-ding-sound": "Шалгалтын жагсаалтын зүйл дуусахад дуу гаргах",
"checklist-ding-sound-description": "Шалгалтын жагсаалтын зүйлийг тэмдэглэхэд богино дуу гаргах. Анхны тохиргоогоор унтраалттай.",
"assignee": "Харюусалгата",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "Холболто бүтэсэгүй",
"cloud-settings-saved": "Хадагаламжын тохируулганууд хадагалагдаба",
"cloud-settings-save-failed": "Хадагаламжын тохируулгануудые хадагалжа шадабагүй",
"cloud": "Үүлэн",
"cloud-settings-desc": "Weddy соо WeKan-тай хамта Cloud Panel нэгэ серверһээ олон WeKan домэнуудые алба хэдэг, тус бүриндэнь өөрын мэдээсэл бии. Cloud Panel домэн дээрэ SuperAdmin домэнуудые болон CloudFlare-ые хүтэлбэрилдэг.",
"cloud-panel-domain": "Cloud Panel домэн",
"cloud-superadmin-username": "SuperAdmin хэрэглэгшын нэрэ",
"cloud-saved": "Хадагалагдаа.",
"cloud-panel-not-allowed": "Cloud Panel гансал SuperAdmin-ай түлөө, Cloud Panel домэн дээрэ.",
"cloud-customer-name": "Клиентын нэрэ",
"cloud-customer-email": "Клиентын сахим захидалай хаяг",
"cloud-billing-email": "Түлбэриин сахим захидалай хаяг",
"cloud-start-date": "Эхилхэ үдэр",
"cloud-end-date": "Дүүрэхэ үдэр",
"cloudflare-api-key": "CloudFlare API түлхюур",
"cloudflare-api-key-desc": "CloudFlare API түлхюуртай һаа, таны CloudFlare данса соо зонотой домэн прокси дамжуулһан DNS бэшэлгэ болон CloudFlare Origin гэршэлгэ абадаг. Бусад домэнууд Let's Encrypt гэршэлгэ абадаг. Түлхюур серверт хадагалагдадаг, дахин хэзээш харуулагдахагүй.",
"cloudflare-api-key-set": "CloudFlare API түлхюур хадагалагдаһан.",
"cloudflare-api-key-not-set": "Ямаршье CloudFlare API түлхюур хадагалагдаагүй: домэн бүхэн Let's Encrypt гэршэлгэ абадаг.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Хабсаргалта Azure Blob Storage руу зөөхэ",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL de l'enllaç del logotip d'inici de sessió personalitzat",
"custom-help-link-url": "URL d'enllaç d'ajuda personalitzat",
"text-below-custom-login-logo": "Text a sota del logotip d'inici de sessió personalitzat",
"login-background-color": "Color de fons de la pàgina d'inici de sessió",
"automatic-linked-url-schemes": "Esquemes d'URL personalitzats que haurien de poder fer clic automàticament. Un esquema d'URL per línia",
"automatic-linked-url-schemes-hint": "Buit per defecte, de manera que només es poden clicar els enllaços web i de correu. Cada esquema indicat, per exemple thunderlink o onenote, obre l'aplicació registrada per gestionar-lo. javascript, data, vbscript i esquemes semblants no es converteixen mai en enllaços.",
"external-link-pattern": "Enllaç automàtic a un rastrejador d'incidències extern",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Quan el títol d'una fitxa nova comença amb [Text] seguit de més text, Text esdevé una etiqueta de la fitxa (es crea si el tauler no en té cap amb aquest nom) i s'elimina del títol. Desactivat: el títol es conserva tal com és.",
"keep-add-card-form-open": "Mantén obert el formulari d'afegir fitxa després d'afegir una fitxa",
"keep-add-card-form-open-description": "Activat: després d'afegir una fitxa, el formulari roman obert amb un títol buit per a la fitxa següent. Desactivat: el formulari es tanca tan bon punt s'afegeix la fitxa.",
"show-focus-outline": "Mostra el contorn del focus",
"checklist-ding-sound": "Reprodueix un so quan es completa un element de la llista de verificació",
"checklist-ding-sound-description": "Reprodueix un so breu quan marqueu un element de la llista de verificació. Desactivat per defecte.",
"assignee": "Assignat",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "La connexió ha fallat",
"cloud-settings-saved": "S'ha desat la configuració de l'emmagatzematge",
"cloud-settings-save-failed": "No s'ha pogut desar la configuració de l'emmagatzematge",
"cloud": "Núvol",
"cloud-settings-desc": "Amb WeKan a Weddy, el Cloud Panel serveix molts dominis de WeKan des d'un sol servidor, cadascun amb les seves dades. Al domini del Cloud Panel, el SuperAdmin gestiona els dominis i CloudFlare.",
"cloud-panel-domain": "Domini del Cloud Panel",
"cloud-superadmin-username": "Nom d'usuari del SuperAdmin",
"cloud-saved": "Desat.",
"cloud-panel-not-allowed": "El Cloud Panel és només per al SuperAdmin, al domini del Cloud Panel.",
"cloud-customer-name": "Nom del client",
"cloud-customer-email": "Adreça de correu electrònic del client",
"cloud-billing-email": "Adreça de correu electrònic de facturació",
"cloud-start-date": "Data d'inici",
"cloud-end-date": "Data de finalització",
"cloudflare-api-key": "Clau API de CloudFlare",
"cloudflare-api-key-desc": "Amb una clau API de CloudFlare, un domini la zona del qual és al vostre compte de CloudFlare obté un registre DNS amb servidor intermediari i un certificat CloudFlare Origin. Els altres dominis obtenen un certificat Let's Encrypt. La clau es desa al servidor i no es torna a mostrar mai més.",
"cloudflare-api-key-set": "S'ha desat una clau API de CloudFlare.",
"cloudflare-api-key-not-set": "No s'ha desat cap clau API de CloudFlare: cada domini obté un certificat Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Mou l'adjunt a Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL de l'enllaç del logotip d'inici de sessió personalitzat",
"custom-help-link-url": "URL d'enllaç d'ajuda personalitzat",
"text-below-custom-login-logo": "Text a sota del logotip d'inici de sessió personalitzat",
"login-background-color": "Color de fons de la pàgina d'inici de sessió",
"automatic-linked-url-schemes": "Esquemes d'URL personalitzats que haurien de poder fer clic automàticament. Un esquema d'URL per línia",
"automatic-linked-url-schemes-hint": "Buit per defecte, de manera que només es poden clicar els enllaços web i de correu. Cada esquema indicat, per exemple thunderlink o onenote, obre l'aplicació registrada per gestionar-lo. javascript, data, vbscript i esquemes semblants no es converteixen mai en enllaços.",
"external-link-pattern": "Enllaç automàtic a un rastrejador d'incidències extern",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Quan el títol d'una fitxa nova comença amb [Text] seguit de més text, Text es convertix en una etiqueta de la fitxa (es crea si el tauler no en té cap amb eixe nom) i s'elimina del títol. Desactivat: el títol es conserva tal com és.",
"keep-add-card-form-open": "Mantín obert el formulari d'afegir fitxa després d'afegir una fitxa",
"keep-add-card-form-open-description": "Activat: després d'afegir una fitxa, el formulari es manté obert amb un títol buit per a la fitxa següent. Desactivat: el formulari es tanca tan prompte com s'afig la fitxa.",
"show-focus-outline": "Mostra el contorn del focus",
"checklist-ding-sound": "Reprodueix un so quan es completa un element de la llista de verificació",
"checklist-ding-sound-description": "Reprodueix un so breu quan marqueu un element de la llista de verificació. Desactivat per defecte.",
"assignee": "Assignat",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "La connexió ha fallat",
"cloud-settings-saved": "S'ha desat la configuració de l'emmagatzematge",
"cloud-settings-save-failed": "No s'ha pogut desar la configuració de l'emmagatzematge",
"cloud": "Núvol",
"cloud-settings-desc": "Amb WeKan en Weddy, el Cloud Panel servix molts dominis de WeKan des d'un sol servidor, cadascun amb les seues dades. En el domini del Cloud Panel, el SuperAdmin gestiona els dominis i CloudFlare.",
"cloud-panel-domain": "Domini del Cloud Panel",
"cloud-superadmin-username": "Nom d'usuari del SuperAdmin",
"cloud-saved": "Desat.",
"cloud-panel-not-allowed": "El Cloud Panel és només per al SuperAdmin, en el domini del Cloud Panel.",
"cloud-customer-name": "Nom del client",
"cloud-customer-email": "Adreça de correu electrònic del client",
"cloud-billing-email": "Adreça de correu electrònic de facturació",
"cloud-start-date": "Data d'inici",
"cloud-end-date": "Data de finalització",
"cloudflare-api-key": "Clau API de CloudFlare",
"cloudflare-api-key-desc": "Amb una clau API de CloudFlare, un domini la zona del qual està en el vostre compte de CloudFlare obté un registre DNS amb servidor intermediari i un certificat CloudFlare Origin. Els altres dominis obtenen un certificat Let's Encrypt. La clau es guarda en el servidor i no es torna a mostrar mai més.",
"cloudflare-api-key-set": "S'ha guardat una clau API de CloudFlare.",
"cloudflare-api-key-not-set": "No s'ha guardat cap clau API de CloudFlare: cada domini obté un certificat Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Mou l'adjunt a Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URL de l'enllaç del logotip d'inici de sessió personalitzat",
"custom-help-link-url": "URL d'enllaç d'ajuda personalitzat",
"text-below-custom-login-logo": "Text a sota del logotip d'inici de sessió personalitzat",
"login-background-color": "Color de fons de la pàgina d'inici de sessió",
"automatic-linked-url-schemes": "Esquemes d'URL personalitzats que haurien de poder fer clic automàticament. Un esquema d'URL per línia",
"automatic-linked-url-schemes-hint": "Buit per defecte, de manera que només es poden clicar els enllaços web i de correu. Cada esquema indicat, per exemple thunderlink o onenote, obre l'aplicació registrada per gestionar-lo. javascript, data, vbscript i esquemes semblants no es converteixen mai en enllaços.",
"external-link-pattern": "Enllaç automàtic a un rastrejador d'incidències extern",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "Quan el títol d'una fitxa nova comença amb [Text] seguit de més text, Text esdevé una etiqueta de la fitxa (es crea si el tauler no en té cap amb aquest nom) i s'elimina del títol. Desactivat: el títol es conserva tal com és.",
"keep-add-card-form-open": "Mantén obert el formulari d'afegir fitxa després d'afegir una fitxa",
"keep-add-card-form-open-description": "Activat: després d'afegir una fitxa, el formulari roman obert amb un títol buit per a la fitxa següent. Desactivat: el formulari es tanca tan bon punt s'afegeix la fitxa.",
"show-focus-outline": "Mostra el contorn del focus",
"checklist-ding-sound": "Reprodueix un so quan es completa un element de la llista de verificació",
"checklist-ding-sound-description": "Reprodueix un so breu quan marqueu un element de la llista de verificació. Desactivat per defecte.",
"assignee": "Assignat",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "La connexió ha fallat",
"cloud-settings-saved": "S'ha desat la configuració de l'emmagatzematge",
"cloud-settings-save-failed": "No s'ha pogut desar la configuració de l'emmagatzematge",
"cloud": "Núvol",
"cloud-settings-desc": "Amb WeKan a Weddy, el Cloud Panel serveix molts dominis de WeKan des d'un sol servidor, cadascun amb les seves dades. Al domini del Cloud Panel, el SuperAdmin gestiona els dominis i CloudFlare.",
"cloud-panel-domain": "Domini del Cloud Panel",
"cloud-superadmin-username": "Nom d'usuari del SuperAdmin",
"cloud-saved": "Desat.",
"cloud-panel-not-allowed": "El Cloud Panel és només per al SuperAdmin, al domini del Cloud Panel.",
"cloud-customer-name": "Nom del client",
"cloud-customer-email": "Adreça de correu electrònic del client",
"cloud-billing-email": "Adreça de correu electrònic de facturació",
"cloud-start-date": "Data d'inici",
"cloud-end-date": "Data de finalització",
"cloudflare-api-key": "Clau API de CloudFlare",
"cloudflare-api-key-desc": "Amb una clau API de CloudFlare, un domini la zona del qual és al vostre compte de CloudFlare obté un registre DNS amb servidor intermediari i un certificat CloudFlare Origin. Els altres dominis obtenen un certificat Let's Encrypt. La clau es desa al servidor i no es torna a mostrar mai més.",
"cloudflare-api-key-set": "S'ha desat una clau API de CloudFlare.",
"cloudflare-api-key-not-set": "No s'ha desat cap clau API de CloudFlare: cada domini obté un certificat Let's Encrypt.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "Mou l'adjunt a Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "ᎤᏩᏒ ᎤᏴᏍᏗ ᎤᏟᎶᏍᏙᏗ ᎤᏓᏚᏓᎸ URL",
"custom-help-link-url": "ᎤᏩᏒ ᎠᏓᏍᏕᎸᏗ ᎤᏓᏚᏓᎸ URL",
"text-below-custom-login-logo": "ᎤᏩᏒ ᎤᏴᏍᏗ ᎤᏟᎶᏍᏙᏗ ᎭᏫᏂ ᏗᎪᏪᎵ",
"login-background-color": "ᎠᏴᏫᏯᎢ ᏔᎵᏍᏙᏗ ᎠᎦᏙᎥᎯᏍᏗ ᎤᏁᎦ",
"automatic-linked-url-schemes": "ᎤᏩᏒ URL Schemes ᎤᏅᏒ ᎠᏂᏐᏅᏍᏗ ᎨᏎᏍᏗ. ᏌᏊ URL Scheme ᏂᎦᎥ ᏗᎪᏪᎵ",
"automatic-linked-url-schemes-hint": "ᎢᎬᏱᎢ ᎠᏒᎭ ᎨᏐᎢ; ᎾᏍᎩᏃ ᎠᏏᎳᏕᏫᏒᎢ ᎪᏪᎵ ᎠᎴ ᎠᎾᎦᎵᏍᎩ ᎪᏪᎵ ᎠᏓᏙᏓᏍᏗ ᎤᏩᏒ ᎠᏐᏅᏍᏙᏗ ᎨᏐᎢ. ᏂᎦᏛ ᎪᏪᎵᎯ URL ᎢᏳᏍᏗ, ᎯᎠ ᎾᏍᎩ thunderlink ᎠᎴᏱᎩ onenote, ᎾᏍᎩ ᎢᏳᏍᏗᏱ ᏫᎦᏬᏪᎳᏅ ᎤᎾᏓᏡᎬᎢ ᎠᏍᏚᎢᏍᎦ. javascript, data, vbscript ᎠᎴ ᎾᏍᎩ ᎢᏳᏍᏗ ᎥᏝ ᎢᎸᎯᏳ ᎠᏓᏙᏓᏍᏗ ᏱᎨᏐᎢ.",
"external-link-pattern": "ᏙᏱᏗᏢ ᎠᏂᏓᏙᎳᏗᏍᎩ ᏗᎦᏘᏲᎲᏍᎩ ᎠᏰᎵ ᎠᏙᏢᏒ",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "ᎢᏳᏃ ᎢᏤ ᎪᏪᎵ ᏓᎬᏔᏂ [ᎪᏪᎸ] ᎤᎴᏅ ᎠᎴ ᏅᏩᏓᎴ ᎪᏪᎸ ᎣᏂ ᏥᎨᏐ, ᎪᏪᎸ ᎪᏪᎵ ᏗᎪᏪᎳᏅᎯ ᏂᎦᎵᏍᏗᏍᎪ (ᎢᏳᏃ ᏗᏟᎶᏍᏙᏗ ᎥᏝ ᎾᏍᎩ ᏚᏙᎥ ᏗᎪᏪᎳᏅᎯ ᏱᎦᎢ, ᎠᏁᏍᎨᏍᏗ) ᎠᎴ ᏓᎬᏔᏂ ᎠᏂᏍᎪ. ᎠᏍᏚᎲ: ᏓᎬᏔᏂ ᏄᏍᏛ ᏂᎦᏓ ᎤᏓᏤᎵᏛ.",
"keep-add-card-form-open": "ᎪᏪᎵ ᎯᏟᏌ ᏗᎪᏪᎵ ᎤᏍᏚᎢᏍᏔᏅ ᎯᏁᎳ ᎪᏪᎵ ᏣᏟᏌᏅ ᎣᏂ",
"keep-add-card-form-open-description": "ᎤᏍᏚᎢᏍᏔᏅ: ᎪᏪᎵ ᏣᏟᏌᏅ ᎣᏂ, ᏗᎪᏪᎵ ᎤᏍᏚᎢᏍᏔᏅ ᎠᏯᎠ ᏄᏍᏛᎾ ᏚᏙᎥ ᎣᏂ ᎪᏪᎵ ᎤᎬᏩᎵ. ᎯᏍᏚᎦ: ᏗᎪᏪᎵ ᎠᏍᏚᎦ ᎪᏪᎵ ᎠᏟᏌᏅ ᎢᏳᏃ.",
"show-focus-outline": "ᎠᏎᏍᏔᏅ ᎠᏍᏓᏩᏛᏍᏗ ᎬᏂᎨᏒ ᎦᏙᎬ",
"checklist-ding-sound": "ᎠᏰᎵ ᏗᎦᎫᏍᏗ ᎠᏓᎴᏂᏍᎬ ᎣᏍᏓ ᏂᎦᎵᏍᏙᏗᎭ",
"checklist-ding-sound-description": "ᎡᎳᏛ ᎣᏍᏓ ᏂᎦᎵᏍᏙᏗᎭ ᎠᏰᎵ ᏗᎦᎫᏍᏗ ᏙᏥᏰᎵᏙᎮᎢ. ᎠᎨᏫ ᎬᏙᏗ ᎥᏝ.",
"assignee": "ᎠᏥᏁᏤᎸ",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "ᎠᏚᏓᎸᏗ Ꮭ ᏱᎨᏎᏍᏗ",
"cloud-settings-saved": "ᎠᏍᏆᏂᎪᏙᏗ ᏗᎧᎿᏩᏛᏍᏗ ᏗᏍᏆᏂᎪᏙᏅ",
"cloud-settings-save-failed": "ᎠᏍᏆᏂᎪᏙᏗ ᏗᎧᎿᏩᏛᏍᏗ ᏗᏍᏆᏂᎪᏙᏗ Ꮭ ᏱᎨᏎᏍᏗ",
"cloud": "ᎤᎶᎩᎸ",
"cloud-settings-desc": "WeKan Weddy ᎠᏂ ᏥᎨᏒ, Cloud Panel ᎤᏂᏣᏘ WeKan ᏗᏓᏁᎸ ᏌᏉ ᎠᏓᏁᎯ ᏅᏓᏳᏓᎴᏅ ᏕᎦᏘᏍᏗᏍᎪ, ᏑᏓᎴᎩ ᎤᏩᏒ ᏧᎾᏙᎴᏆᏍᏗ ᎤᏁᎯ. Cloud Panel ᏗᏓᏁᎸ ᎾᎿ, SuperAdmin ᏕᎦᏁᎶᏗᏍᎪ ᏗᏓᏁᎸ ᎠᎴ CloudFlare.",
"cloud-panel-domain": "Cloud Panel ᏗᏓᏁᎸ",
"cloud-superadmin-username": "SuperAdmin ᎤᏬᏢᏗ ᏚᏙᎥ",
"cloud-saved": "ᎠᏍᏆᏂᎪᏔᏅᎯ.",
"cloud-panel-not-allowed": "Cloud Panel SuperAdmin ᎤᏩᏒᏉ ᎤᎬᏩᎶᏗ, Cloud Panel ᏗᏓᏁᎸ ᎾᎿ.",
"cloud-customer-name": "ᎠᏓᏩᏛᎯ ᏚᏙᎥ",
"cloud-customer-email": "ᎠᏓᏩᏛᎯ ᏗᎦᎵᏦᏙᏗ ᎦᏙᎬ",
"cloud-billing-email": "ᎠᏴᎾᏗ ᏗᎦᎵᏦᏙᏗ ᎦᏙᎬ",
"cloud-start-date": "ᏗᏓᎴᏂᏍᎬ ᎢᎦ",
"cloud-end-date": "ᎤᎵᏍᏆᏗᏍᏗ ᎢᎦ",
"cloudflare-api-key": "CloudFlare API ᏗᏍᏚᎢᏍᏗ",
"cloudflare-api-key-desc": "CloudFlare API ᏗᏍᏚᎢᏍᏗ ᏤᎭ ᎢᏳᏃ, ᏗᏓᏁᎸ ᎾᏍᎩ ᎤᏤᎵ zone ᏣᏤᎵ CloudFlare ᎾᎿ ᏤᎭ ᎠᏩᏘᏍᎪ proxy DNS ᎪᏪᎵ ᎠᎴ CloudFlare Origin ᎪᏪᎵ. ᏅᏩᎾᏓᎴ ᏗᏓᏁᎸ ᎠᏂᏩᏘᏍᎪ Let's Encrypt ᎪᏪᎵ. ᏗᏍᏚᎢᏍᏗ ᎠᏓᏁᎯ ᎾᎿ ᎠᏍᏆᏂᎪᏗ ᎠᎴ ᎥᏝ ᎤᏟ ᎢᎸᎯᏳ ᎬᏂᎨᏒ ᏱᎨᏎᏍᏗ.",
"cloudflare-api-key-set": "CloudFlare API ᏗᏍᏚᎢᏍᏗ ᎠᏍᏆᏂᎪᏗ.",
"cloudflare-api-key-not-set": "ᎥᏝ CloudFlare API ᏗᏍᏚᎢᏍᏗ ᏯᏍᏆᏂᎪᏗ: ᏂᎦᏓ ᏗᏓᏁᎸ ᎠᏩᏘᏍᎪ Let's Encrypt ᎪᏪᎵ.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "ᎦᏛᎯᏍᏔᏅ Azure Blob Storage ᏫᎦᏅᏗ",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "URLی بەستەری لۆگۆی تایبەتی چوونەژوورەوە",
"custom-help-link-url": "URLی بەستەری یارمەتی تایبەت",
"text-below-custom-login-logo": "دەق لەژێر لۆگۆی تایبەتی چوونەژوورەوە",
"login-background-color": "ڕەنگی پاشخانی پەڕەی چوونەژوورەوە",
"automatic-linked-url-schemes": "شێوازە تایبەتییەکانی URL کە دەبێت خۆکارانە کرتەیان لێبکرێت. هەر دێڕێک یەک شێوازی URL",
"automatic-linked-url-schemes-hint": "بە بنەڕەت بەتاڵە، بۆیە تەنها بەستەرەکانی وێب و ئیمەیڵ کلیکیان لەسەر دەکرێت. هەر شێوەی بەستەرێکی ناو لیستەکە، بۆ نموونە thunderlink یان onenote، ئەو بەرنامەیە دەکاتەوە کە بۆی تۆمار کراوە. javascript و data و vbscript و شێوە هاوشێوەکان هەرگیز ناکرێن بە بەستەر.",
"external-link-pattern": "پەیوەندی خۆکار بە شوێنپێگری کێشەی دەرەکی",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "کاتێک سەردێڕی کارتێکی نوێ بە [دەق] دەست پێدەکات و دەقی تری بەدوادا دێت، دەق دەبێتە پێناسەی کارتەکە (دروست دەکرێت ئەگەر تەختەکە پێناسەیەکی بەو ناوەی نەبێت) و لە سەردێڕەکە لادەبرێت. کوژاوە: سەردێڕەکە وەک خۆی دەمێنێتەوە.",
"keep-add-card-form-open": "فۆرمی زیادکردنی کارت دوای زیادکردنی کارتێک بە کراوەیی بهێڵەرەوە",
"keep-add-card-form-open-description": "چالاک: دوای زیادکردنی کارتێک، فۆرمەکە بە ناونیشانێکی بەتاڵ بۆ کارتی داهاتوو کراوە دەمێنێتەوە. ناچالاک: فۆرمەکە هەر کە کارتەکە زیادکرا دادەخرێت.",
"show-focus-outline": "نیشاندانی هێڵی دەورەی فۆکەس",
"checklist-ding-sound": "کاتێک بابەتێکی لیستی پشکنین تەواو دەبێت دەنگ لێبدە",
"checklist-ding-sound-description": "کاتێک بابەتێکی لیستی پشکنین دەپشکنیت دەنگێکی کورت لێبدە. بە بنەڕەت داخراوە.",
"assignee": "بەرپرس",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "پەیوەندی شکستی هێنا",
"cloud-settings-saved": "ڕێکخستنەکانی بیرگە پاشەکەوت کران",
"cloud-settings-save-failed": "پاشەکەوتکردنی ڕێکخستنەکانی بیرگە شکستی هێنا",
"cloud": "هەور",
"cloud-settings-desc": "بە WeKan لە Weddy، Cloud Panel زۆر دۆمەینی WeKan لە یەک ڕاژەکارەوە پێشکەش دەکات، هەر یەکەیان داتای خۆی هەیە. لە دۆمەینی Cloud Panel، SuperAdmin دۆمەینەکان و CloudFlare بەڕێوە دەبات.",
"cloud-panel-domain": "دۆمەینی Cloud Panel",
"cloud-superadmin-username": "ناوی بەکارهێنەری SuperAdmin",
"cloud-saved": "پاشەکەوت کرا.",
"cloud-panel-not-allowed": "Cloud Panel تەنها بۆ SuperAdmin ە، لە دۆمەینی Cloud Panel.",
"cloud-customer-name": "ناوی کڕیار",
"cloud-customer-email": "ناونیشانی ئیمەیلی کڕیار",
"cloud-billing-email": "ناونیشانی ئیمەیلی پسوولە",
"cloud-start-date": "ڕێکەوتی دەستپێک",
"cloud-end-date": "ڕێکەوتی کۆتایی",
"cloudflare-api-key": "کلیلی CloudFlare API",
"cloudflare-api-key-desc": "بە کلیلی CloudFlare API، دۆمەینێک کە ناوچەکەی لە هەژماری CloudFlare ەکەتدایە تۆمارێکی DNS ی پرۆکسیکراو و بڕوانامەیەکی CloudFlare Origin وەردەگرێت. دۆمەینەکانی تر بڕوانامەیەکی Let's Encrypt وەردەگرن. کلیلەکە لەسەر ڕاژەکار پاشەکەوت دەکرێت و هەرگیز دووبارە پیشان نادرێتەوە.",
"cloudflare-api-key-set": "کلیلێکی CloudFlare API پاشەکەوت کراوە.",
"cloudflare-api-key-not-set": "هیچ کلیلێکی CloudFlare API پاشەکەوت نەکراوە: هەموو دۆمەینێک بڕوانامەیەکی Let's Encrypt وەردەگرێت.",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "هاوپێچ بگوازەوە بۆ Azure Blob Storage",
+17
View File
@@ -1311,6 +1311,7 @@
"custom-login-logo-link-url": "自定义登录徽标链接 URL",
"custom-help-link-url": "自定义帮助链接 URL",
"text-below-custom-login-logo": "自定义登录徽标下方的文字",
"login-background-color": "登录页面背景颜色",
"automatic-linked-url-schemes": "应自动可点击的自定义 URL 方案。每行一个 URL 方案",
"automatic-linked-url-schemes-hint": "默认为空,因此只有网页和邮件链接可以点击。列出的每个协议(例如 thunderlink 或 onenote)都会打开为其注册的应用程序。javascript、data、vbscript 及类似协议永远不会转换为链接。",
"external-link-pattern": "自动链接到外部问题跟踪系统",
@@ -1991,6 +1992,7 @@
"title-bracket-label-description": "当新卡片的标题以 [文本] 开头且后面还有其他文字时,文本会成为该卡片的标签(如果看板中没有该名称的标签则会创建),并从标题中删除。关闭:标题保持原样。",
"keep-add-card-form-open": "添加卡片后保持“添加卡片”表单打开",
"keep-add-card-form-open-description": "开:添加卡片后,表单保持打开,并为下一张卡片留出空白标题。关:添加卡片后表单即关闭。",
"show-focus-outline": "显示焦点轮廓",
"checklist-ding-sound": "勾选清单项目时播放声音",
"checklist-ding-sound-description": "勾选清单项目时播放一声短促的叮咚提示音。默认关闭。",
"assignee": "受让人",
@@ -2963,6 +2965,21 @@
"cloud-connection-failed": "连接失败",
"cloud-settings-saved": "存储设置已保存",
"cloud-settings-save-failed": "保存存储设置失败",
"cloud": "云",
"cloud-settings-desc": "在 Weddy 中使用 WeKan 时,Cloud Panel 从一台服务器为多个 WeKan 域名提供服务,每个域名都有自己的数据。在 Cloud Panel 域名上,SuperAdmin 管理这些域名和 CloudFlare。",
"cloud-panel-domain": "Cloud Panel 域名",
"cloud-superadmin-username": "SuperAdmin 用户名",
"cloud-saved": "已保存。",
"cloud-panel-not-allowed": "Cloud Panel 仅供 SuperAdmin 在 Cloud Panel 域名上使用。",
"cloud-customer-name": "客户名称",
"cloud-customer-email": "客户电子邮件地址",
"cloud-billing-email": "账单电子邮件地址",
"cloud-start-date": "开始日期",
"cloud-end-date": "结束日期",
"cloudflare-api-key": "CloudFlare API 密钥",
"cloudflare-api-key-desc": "使用 CloudFlare API 密钥后,其区域位于您的 CloudFlare 账户中的域名将获得代理的 DNS 记录和 CloudFlare Origin 证书。其他域名将获得 Let's Encrypt 证书。密钥保存在服务器上,且不会再次显示。",
"cloudflare-api-key-set": "已保存 CloudFlare API 密钥。",
"cloudflare-api-key-not-set": "未保存 CloudFlare API 密钥:每个域名都将获得 Let's Encrypt 证书。",
"move-storage-azure": "Azure Blob",
"move-storage-gcs": "Google Cloud Storage",
"attachment-move-storage-azure": "将附件移动到 Azure Blob Storage",
Loaded 100 of 552 files, more files were not shown because too many files have changed in this diff. Show more