mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 03:22:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
246ba44838 | ||
|
|
1eda4f256e | ||
|
|
f83ee4d320 | ||
|
|
e43dc0070f | ||
|
|
579de7d6af | ||
|
|
fb8021ba96 | ||
|
|
fe0e187fa0 | ||
|
|
d1d3beb12d | ||
|
|
e33b3f26d3 | ||
|
|
1211221a0f | ||
|
|
c00c602441 | ||
|
|
47a19e3842 | ||
|
|
73db2a28c3 | ||
|
|
11bed26788 | ||
|
|
6a64d374c6 | ||
|
|
8f15879816 | ||
|
|
74339a8a82 | ||
|
|
36a1939653 | ||
|
|
a61a40a5db | ||
|
|
4fd3217aee | ||
|
|
5b436a18dc | ||
|
|
2e87a775ae | ||
|
|
f653d619f5 | ||
|
|
da731aa2fe | ||
|
|
a42b63d711 | ||
|
|
a14923cac3 | ||
|
|
fdcb26096b | ||
|
|
b59152a079 | ||
|
|
1595155af2 | ||
|
|
f4f4c7f2cb | ||
|
|
97cc91cd67 | ||
|
|
33c2ad8fb0 | ||
|
|
c936e7708d | ||
|
|
5c1f6088f4 | ||
|
|
ceb20a81fc | ||
|
|
2128e6e02f | ||
|
|
49ee37a3a8 | ||
|
|
19350f2611 | ||
|
|
2306a36814 | ||
|
|
a4d2313a68 | ||
|
|
570cc34552 | ||
|
|
7ae45651dc | ||
|
|
db52764592 | ||
|
|
2b99ffe288 | ||
|
|
b4b130858f | ||
|
|
0275363043 | ||
|
|
42d8341d47 | ||
|
|
a9aaa22592 | ||
|
|
4d5fe63fc4 | ||
|
|
1f12b20291 | ||
|
|
bf8ecbbc19 | ||
|
|
e89a3450cd | ||
|
|
7e4483b1a9 | ||
|
|
a4eacfd9bf | ||
|
|
b4cb989391 | ||
|
|
60377a4dc2 | ||
|
|
5c45d55d70 | ||
|
|
e5a84cbcef | ||
|
|
1ef7a57ce4 | ||
|
|
9caf0c8653 | ||
|
|
adb73fd8ee | ||
|
|
ea5724babd | ||
|
|
b895e61237 | ||
|
|
3108e017fc | ||
|
|
081690875d | ||
|
|
68b575637d | ||
|
|
6d5f660c27 | ||
|
|
f54aa93dc1 | ||
|
|
e3458004ec | ||
|
|
6ae4ad3545 | ||
|
|
b0a71ba57b | ||
|
|
f3d013339c | ||
|
|
95ea04b049 | ||
|
|
3b33c7d7ba | ||
|
|
4762bed8c1 | ||
|
|
029ecb6b39 | ||
|
|
49c309b324 | ||
|
|
e09267cc90 | ||
|
|
b46f155fa3 | ||
|
|
0c0288a6c5 | ||
|
|
c61a1b7f41 | ||
|
|
9ce35869a7 | ||
|
|
2017d07e37 | ||
|
|
9ea5c81932 | ||
|
|
45ccdd9c3c | ||
|
|
5a21b074d4 | ||
|
|
e9c5888485 | ||
|
|
09c9d72398 | ||
|
|
4f3d45f3de | ||
|
|
3aaa46fdae | ||
|
|
59f1b705e0 | ||
|
|
22e5df55d7 | ||
|
|
016b4bf3bd | ||
|
|
4d44349ab6 | ||
|
|
d3edc56170 | ||
|
|
5134e3d2a8 | ||
|
|
f686b76023 | ||
|
|
cd2adcad02 | ||
|
|
0766a65fef | ||
|
|
58ad81e301 | ||
|
|
2912f4b080 | ||
|
|
97d1eca09d | ||
|
|
95b75177b8 | ||
|
|
880ec2d707 | ||
|
|
dbc7cccea3 | ||
|
|
d8b3579117 | ||
|
|
c7e53e9f38 | ||
|
|
6012521ccc | ||
|
|
0195a5b3d4 | ||
|
|
b12e761afb | ||
|
|
0c0d666ffa | ||
|
|
fc2ad0a48b | ||
|
|
dad03959d9 | ||
|
|
ac960e7d81 | ||
|
|
3213d7fa1d | ||
|
|
4757676abf | ||
|
|
efaf94242e | ||
|
|
d87baa0244 | ||
|
|
006d24502c | ||
|
|
8f4efbca57 | ||
|
|
dde09dead5 | ||
|
|
3621413d6f | ||
|
|
3c3fc07b3c | ||
|
|
c1e8976b28 | ||
|
|
dcce935310 | ||
|
|
1b05673200 | ||
|
|
dafb63299f | ||
|
|
84b0480aa8 | ||
|
|
f6fc97e77a | ||
|
|
d7105e531d | ||
|
|
f25957dcf3 | ||
|
|
984ea93aa8 | ||
|
|
2441563008 | ||
|
|
f3950b9adc | ||
|
|
5e07b8db6c | ||
|
|
eeeb0dbec9 | ||
|
|
96d56ecc6c | ||
|
|
aadfd9b6b3 | ||
|
|
1c44876beb | ||
|
|
bd86b518ea | ||
|
|
f576a0cca9 | ||
|
|
46c0ad43e8 | ||
|
|
499772e4c2 | ||
|
|
90fe133363 | ||
|
|
b812d36871 | ||
|
|
d05a42a5af | ||
|
|
42507de1d0 | ||
|
|
131b0cc509 | ||
|
|
93ae5ac6ee | ||
|
|
34bb91d1f5 | ||
|
|
d231df56fc | ||
|
|
e422ab691e | ||
|
|
3f712a0445 | ||
|
|
d99dadadee | ||
|
|
a58b7bee2c | ||
|
|
62da564a1e | ||
|
|
2ada2e2d61 | ||
|
|
5de1acf496 | ||
|
|
3008bb5606 | ||
|
|
4f7521ae3c | ||
|
|
1c63049913 | ||
|
|
6b27764452 | ||
|
|
ae49937b44 | ||
|
|
7f27ea1f26 | ||
|
|
144777ae6b | ||
|
|
f4b5025a7b |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=$?
|
||||
|
||||
@@ -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
@@ -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/
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+1099
File diff suppressed because it is too large.
Load diff
+36
-26
@@ -5,43 +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.89 2026-11-03 WeKan ® release
|
||||
# v13.01 2026-11-08 WeKan ® release
|
||||
|
||||
Keyboard **shortcuts run once on a non-Latin keyboard**: `d` no
|
||||
longer opens the Due date popup and closes it again on a Farsi layout.
|
||||
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.89](CHANGELOG-big.md#v1289-2026-11-03-wekan--release)
|
||||
[All changes in v13.01](CHANGELOG-big.md#v1301-2026-11-08-wekan--release)
|
||||
|
||||
# v12.88 2026-11-03 WeKan ® release
|
||||
# v13.00 2026-11-07 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.
|
||||
**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.88](CHANGELOG-big.md#v1288-2026-11-03-wekan--release)
|
||||
[All changes in v13.00](CHANGELOG-big.md#v1300-2026-11-07-wekan--release)
|
||||
|
||||
# v12.87 2026-11-03 WeKan ® release
|
||||
# v12.99 2026-11-06 WeKan ® release
|
||||
|
||||
A **REST API** "get one" that finds nothing answers **404**
|
||||
instead of 200 with an empty body.
|
||||
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.87](CHANGELOG-big.md#v1287-2026-11-03-wekan--release)
|
||||
[All changes in v12.99](CHANGELOG-big.md#v1299-2026-11-06-wekan--release)
|
||||
|
||||
# v12.86 2026-11-02 WeKan ® release
|
||||
# v12.98 2026-11-06 WeKan ® release
|
||||
|
||||
The daily **notification cleanup** and the **file integrity
|
||||
audit**
|
||||
run once for the whole installation, not once per server.
|
||||
**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.86](CHANGELOG-big.md#v1286-2026-11-02-wekan--release)
|
||||
[All changes in v12.98](CHANGELOG-big.md#v1298-2026-11-06-wekan--release)
|
||||
|
||||
# v12.85 2026-11-02 WeKan ® release
|
||||
# v12.97 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.
|
||||
`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.85](CHANGELOG-big.md#v1285-2026-11-02-wekan--release)
|
||||
[All changes in v12.97](CHANGELOG-big.md#v1297-2026-11-06-wekan--release)
|
||||
@@ -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
@@ -38,7 +38,7 @@ FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=12.89
|
||||
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
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v12.89.0"
|
||||
appVersion: "v13.01.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -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 ===========================================================================
|
||||
|
||||
@@ -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'}}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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'],
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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'}}")
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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); });
|
||||
@@ -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.
|
||||
|
||||
@@ -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.89-win64.zip](https://github.com/wekan/wekan/releases/download/v12.89/wekan-12.89-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.89-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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+621
-604
File diff suppressed because it is too large.
Load diff
@@ -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-লৈ স্থানান্তৰ কৰক",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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-ға күсерергә",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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-এ সরান",
|
||||
|
||||
@@ -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 ལ་སྤོ།",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 руу зөөхэ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ᏫᎦᏅᏗ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1311,6 +1311,7 @@
|
||||
"custom-login-logo-link-url": "URL di u ligame di u logò di cunnessione persunalizatu",
|
||||
"custom-help-link-url": "URL di u ligame d’aiutu persunalizatu",
|
||||
"text-below-custom-login-logo": "Testu sottu à u logò di cunnessione persunalizatu",
|
||||
"login-background-color": "Culore di fondu di a pagina d'accessu",
|
||||
"automatic-linked-url-schemes": "Schemi URL persunalizati chì devenu esse clicchevuli autumaticamente. Un schema URL per linea",
|
||||
"automatic-linked-url-schemes-hint": "Viotu per difettu, dunque solu i ligami web è di posta sò clicchevuli. Ogni schema elencatu, per esempiu thunderlink o onenote, apre l'applicazione arregistrata per ellu. javascript, data, vbscript è i schemi simili ùn diventanu mai ligami.",
|
||||
"external-link-pattern": "Ligame autumaticu à un tracker di prublemi esternu",
|
||||
@@ -1991,6 +1992,7 @@
|
||||
"title-bracket-label-description": "Quandu u titulu di una carta nova principia cù [Testu] seguitu da altru testu, Testu diventa un'etichetta di a carta (creata s'è a tavula ùn ne hà micca cù stu nome) è hè cacciatu da u titulu. Spentu: u titulu ferma cum'ellu hè.",
|
||||
"keep-add-card-form-open": "Tene apertu u furmulariu Aghjunghje una carta dopu avè aghjuntu una carta",
|
||||
"keep-add-card-form-open-description": "Attivatu: dopu chì una carta hè aghjunta, u furmulariu ferma apertu cù un titulu viotu per a carta seguente. Disattivatu: u furmulariu si chjode appena chì a carta hè aghjunta.",
|
||||
"show-focus-outline": "Mustrà u cuntornu di u focu",
|
||||
"checklist-ding-sound": "Ghjucà un sonu quandu un elementu di a lista di verificazione hè cumpletatu",
|
||||
"checklist-ding-sound-description": "Ghjucà un sonu cortu quandu marcate un elementu di a lista di verificazione. Disattivatu per difettu.",
|
||||
"assignee": "Assignatu",
|
||||
@@ -2963,6 +2965,21 @@
|
||||
"cloud-connection-failed": "Cunnessione fiascata",
|
||||
"cloud-settings-saved": "Parametri d’almacenamentu arregistrati",
|
||||
"cloud-settings-save-failed": "Ùn hè statu pussibule d’arregistrà i paràmetri d’almacenamentu",
|
||||
"cloud": "Nivulu",
|
||||
"cloud-settings-desc": "Cù WeKan in Weddy, u Cloud Panel serve parechji duminii WeKan da un solu servitore, ognunu cù i so dati. À u duminiu di u Cloud Panel, u SuperAdmin gestisce i duminii è CloudFlare.",
|
||||
"cloud-panel-domain": "Duminiu di u Cloud Panel",
|
||||
"cloud-superadmin-username": "Nome d'utilizatore di u SuperAdmin",
|
||||
"cloud-saved": "Salvatu.",
|
||||
"cloud-panel-not-allowed": "U Cloud Panel hè solu per u SuperAdmin, à u duminiu di u Cloud Panel.",
|
||||
"cloud-customer-name": "Nome di u cliente",
|
||||
"cloud-customer-email": "Indirizzu elettronicu di u cliente",
|
||||
"cloud-billing-email": "Indirizzu elettronicu di fatturazione",
|
||||
"cloud-start-date": "Data di principiu",
|
||||
"cloud-end-date": "Data di fine",
|
||||
"cloudflare-api-key": "Chjave API CloudFlare",
|
||||
"cloudflare-api-key-desc": "Cù una chjave API CloudFlare, un duminiu chì a so zona hè in u vostru contu CloudFlare riceve un arregistramentu DNS per proxy è un certificatu CloudFlare Origin. L'altri duminii ricevenu un certificatu Let's Encrypt. A chjave hè salvata nant'à u servitore è ùn hè mai più mustrata.",
|
||||
"cloudflare-api-key-set": "Una chjave API CloudFlare hè salvata.",
|
||||
"cloudflare-api-key-not-set": "Nisuna chjave API CloudFlare hè salvata: ogni duminiu riceve un certificatu Let's Encrypt.",
|
||||
"move-storage-azure": "Azure Blob",
|
||||
"move-storage-gcs": "Google Cloud Storage",
|
||||
"attachment-move-storage-azure": "Move l’appicciu in Azure Blob Storage",
|
||||
|
||||
Loaded 100 of 549 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user