Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4c0539af
|
||
|
|
b8fcef2fd4
|
||
|
|
4e2e6adc73 | ||
|
|
091541a960 | ||
|
|
967b8482d3 | ||
|
|
cb43837c32 | ||
|
|
899617bd4d | ||
|
|
2757bfb365 | ||
|
|
1ed942bb8a | ||
|
|
179157ccd2 | ||
|
|
3e36b931a6 | ||
|
|
c9e60847d0 | ||
|
|
4d83dc0c05 | ||
|
|
995965d157 | ||
|
|
15fbb314b0 | ||
|
|
68e0032c6d | ||
|
|
cffca535b5 | ||
|
|
b2d2632e3b | ||
|
|
119379d4b4 | ||
|
|
c434f83dfc | ||
|
|
1527f364b1 | ||
|
|
5779d94d78 | ||
|
|
096a3dc46b | ||
|
|
c883f31d35 | ||
|
|
1fa9714d4e | ||
|
|
53b64ee222 | ||
|
|
e4b8396a3b | ||
|
|
8cf7e83493 | ||
|
|
68220f944b | ||
|
|
fc4d84574e | ||
|
|
2989e1d5b1 | ||
|
|
5844e20d55 | ||
|
|
2376237b6e | ||
|
|
bcf55bdc3a | ||
|
|
ac44706461 | ||
|
|
928db8d675 | ||
|
|
5c9407f729 | ||
|
|
93f50a7fb3 | ||
|
|
39d84f8fe8 | ||
|
|
d369a3614a | ||
|
|
078ab4125e | ||
|
|
3f0dc8b7be | ||
|
|
c020e09aff | ||
|
|
b0c007d44f | ||
|
|
b777ee2a12 | ||
|
|
b328cae552 | ||
|
|
5357bb47c3 | ||
|
|
447877733b | ||
|
|
feb491f8d4 | ||
|
|
8f59d76a4e | ||
|
|
3a52565ab4 | ||
|
|
08ae61161c | ||
|
|
1835245943 | ||
|
|
83de473e4e | ||
|
|
a532ee7f14 | ||
|
|
f00b265b34 | ||
|
|
2931cc990f | ||
|
|
25a4672d6d | ||
|
|
969a4016ee | ||
|
|
5c2d7079ce | ||
|
|
d1c71d0526 | ||
|
|
1fa2ab0146 | ||
|
|
26a03f4845 | ||
|
|
58bfad3adf | ||
|
|
f1c2f1f40f | ||
|
|
7d3e8eb89f | ||
|
|
3fe8f602c8 | ||
|
|
d87eed36a0 | ||
|
|
9cc0660410 | ||
|
|
58ff88890d | ||
|
|
77d60b21a1 | ||
|
|
9df2db0314 | ||
|
|
91839cf3ef | ||
|
|
74f2352387 | ||
|
|
f299ca1285 | ||
|
|
f47e67b5b4 | ||
|
|
fe16b94c1e | ||
|
|
b77b62bfa2 | ||
|
|
43d8bf1b92 | ||
|
|
dca9427bfb | ||
|
|
7f17616b75 | ||
|
|
4a9dcdd0ef | ||
|
|
a1a4176ffb | ||
|
|
a86a087915 | ||
|
|
f9ace4cde9 | ||
|
|
4677324425 | ||
|
|
7e5852c62c | ||
|
|
e01f99eb52 | ||
|
|
8ff9a41f2d | ||
|
|
075e86b00e | ||
|
|
f20080a603 | ||
|
|
3594d75b5d | ||
|
|
72767ad9a7 | ||
|
|
7db386fda3 | ||
|
|
39f3c89b0a | ||
|
|
15a598525f | ||
|
|
d30803276d | ||
|
|
be63a357dd | ||
|
|
c71a97cba2 | ||
|
|
357de728c0 | ||
|
|
db3c0c18f9 | ||
|
|
45706292e7 | ||
|
|
29b3f52e50 | ||
|
|
f81a0d1537 | ||
|
|
e613e8763b | ||
|
|
3d17810293 | ||
|
|
1871873da0 | ||
|
|
15b3213a4d | ||
|
|
4265288417 | ||
|
|
a36d6f9326 | ||
|
|
3c50f90b51 | ||
|
|
24a4b8e5ee | ||
|
|
e676f96905 | ||
|
|
a903154ab2 | ||
|
|
beedf2fefa | ||
|
|
dee5711000 | ||
|
|
e93c049e8c | ||
|
|
fc10ce4259 |
No files matched your search
+2
-1
@@ -1,3 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pull requests request review from the eligible staff team(s).
|
||||
# One approval from either an Admin or Developer satisfies branch protection.
|
||||
* @trueog/Admins @trueog/Developers
|
||||
@@ -26,13 +26,33 @@ jobs:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
|
||||
restore-keys: meteor-
|
||||
|
||||
- name: Cache Meteor build (.meteor/local)
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .meteor/local
|
||||
key: meteor-local-unit-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: meteor-local-unit-
|
||||
|
||||
- name: Install Meteor
|
||||
run: curl https://install.meteor.com/ | sh
|
||||
run: |
|
||||
if [ ! -x ~/.meteor/meteor ]; then
|
||||
curl https://install.meteor.com/ | sh
|
||||
fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Meteor unit tests
|
||||
# First Meteor build is slow; allow time before the test driver starts.
|
||||
timeout-minutes: 45
|
||||
run: meteor test --once --driver-package meteortesting:mocha
|
||||
env:
|
||||
TEST_WATCH: '0'
|
||||
@@ -66,8 +86,26 @@ jobs:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
|
||||
restore-keys: meteor-
|
||||
|
||||
- name: Cache Meteor build (.meteor/local)
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .meteor/local
|
||||
key: meteor-local-e2e-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: meteor-local-e2e-
|
||||
|
||||
- name: Install Meteor
|
||||
run: curl https://install.meteor.com/ | sh
|
||||
run: |
|
||||
if [ ! -x ~/.meteor/meteor ]; then
|
||||
curl https://install.meteor.com/ | sh
|
||||
fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install app npm dependencies
|
||||
run: npm ci
|
||||
@@ -85,13 +123,22 @@ jobs:
|
||||
MONGO_URL=mongodb://localhost:27017/wekan_test \
|
||||
ROOT_URL=http://localhost:3000 \
|
||||
PORT=3000 \
|
||||
meteor run --settings settings.json &
|
||||
# Wait up to 120 s for WeKan to become available
|
||||
for i in $(seq 1 24); do
|
||||
curl -sf http://localhost:3000 > /dev/null && break
|
||||
echo "Waiting for WeKan... attempt $i"
|
||||
sleep 5
|
||||
meteor run --settings settings.json > wekan-run.log 2>&1 &
|
||||
# The first Meteor build is slow, so wait up to 20 minutes for the
|
||||
# server to start responding before giving up.
|
||||
for i in $(seq 1 120); do
|
||||
if curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan is up after $((i * 10))s"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for WeKan... attempt $i/120"
|
||||
sleep 10
|
||||
done
|
||||
if ! curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan failed to start. Last 200 log lines:"
|
||||
tail -n 200 wekan-run.log
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
DISABLE_CAPTCHA: 'true'
|
||||
|
||||
@@ -145,8 +192,26 @@ jobs:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
|
||||
restore-keys: meteor-
|
||||
|
||||
- name: Cache Meteor build (.meteor/local)
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .meteor/local
|
||||
key: meteor-local-regression-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: meteor-local-regression-
|
||||
|
||||
- name: Install Meteor
|
||||
run: curl https://install.meteor.com/ | sh
|
||||
run: |
|
||||
if [ ! -x ~/.meteor/meteor ]; then
|
||||
curl https://install.meteor.com/ | sh
|
||||
fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
@@ -159,11 +224,21 @@ jobs:
|
||||
MONGO_URL=mongodb://localhost:27017/wekan_regression \
|
||||
ROOT_URL=http://localhost:3000 \
|
||||
PORT=3000 \
|
||||
meteor run &
|
||||
for i in $(seq 1 24); do
|
||||
curl -sf http://localhost:3000 > /dev/null && break
|
||||
sleep 5
|
||||
meteor run > wekan-run.log 2>&1 &
|
||||
# The first Meteor build is slow, so wait up to 20 minutes.
|
||||
for i in $(seq 1 120); do
|
||||
if curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan is up after $((i * 10))s"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for WeKan... attempt $i/120"
|
||||
sleep 10
|
||||
done
|
||||
if ! curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan failed to start. Last 200 log lines:"
|
||||
tail -n 200 wekan-run.log
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
DISABLE_CAPTCHA: 'true'
|
||||
|
||||
|
||||
+398
-252
@@ -1,40 +1,115 @@
|
||||
name: Release All Platforms
|
||||
|
||||
# Triggered manually via GitHub CLI or Actions UI.
|
||||
# Triggered manually via GitHub CLI or the Actions UI.
|
||||
# Usage:
|
||||
# gh workflow run release-all.yml -f old_version=8.42 -f new_version=8.43
|
||||
# gh workflow run release-all.yml -f old_version=9.35 -f new_version=9.36
|
||||
#
|
||||
# Or use the convenience script (commits pending changes, pushes, and triggers):
|
||||
# ./releases/release-all.sh 8.42 8.43
|
||||
# Or use the convenience script (bumps versions, commits, pushes, then triggers):
|
||||
# ./releases/release-all.sh 9.35 9.36
|
||||
#
|
||||
# Builds only the platforms MongoDB ships server binaries for (amd64, arm64,
|
||||
# win64, mac); MongoDB 7.x is the default database. s390x/ppc64le are not built
|
||||
# because MongoDB has no server binaries for them.
|
||||
#
|
||||
# This workflow does EVERYTHING for a release remotely (nothing is built or
|
||||
# pushed on the maintainer's machine), running jobs in parallel where possible:
|
||||
# - bump: runs releases/version.sh + releases/rebuild-docs.sh on the runner
|
||||
# (bumps Node/MongoDB/WeKan versions, rebuilds API docs), then
|
||||
# commits and pushes the bump to wekan/wekan main
|
||||
# - prepare: tags the bumped main as v<new_version> and extracts release notes
|
||||
# - website: updates the wekan/wekan.fi repo (install page + API docs) and pushes
|
||||
# - charts: updates and publishes the wekan/charts Helm chart and pushes
|
||||
# - build-*: builds the amd64 bundle (full Meteor 3 build), then rebuilds native
|
||||
# modules for arm64, win64, mac-arm64 from that amd64 base
|
||||
# - release: creates the GitHub Release with the newest CHANGELOG.md notes and
|
||||
# uploads every wekan-<version>-<platform>.zip bundle
|
||||
# - docker: builds and pushes the multi-arch image to Docker Hub, Quay.io, GHCR
|
||||
# - snap: builds the snap on Launchpad (amd64 + arm64), pushes it to the Snap
|
||||
# Store and attaches the .snap files to the GitHub Release
|
||||
#
|
||||
# Required GitHub Secrets:
|
||||
# GHCR_AUTH - base64(username:token) for ghcr.io
|
||||
# DOCKERHUB_AUTH - base64(username:token) for Docker Hub
|
||||
# QUAY_AUTH - base64(username:token) for Quay.io
|
||||
# SNAP_AUTH - Snapcraft store credentials
|
||||
# GHCR_AUTH - base64("username:token") for ghcr.io
|
||||
# DOCKERHUB_AUTH - base64("username:token") for Docker Hub
|
||||
# QUAY_AUTH - base64("username:token") for Quay.io
|
||||
# SNAP_AUTH - Snap Store credentials (output of `snapcraft export-login`)
|
||||
# LP_CREDENTIALS - base64 of the Launchpad credentials file used by
|
||||
# `snapcraft remote-build` (see releases/snap-build.sh notes)
|
||||
# WEKAN_REPO_TOKEN - Personal Access Token with write access to wekan/wekan.fi
|
||||
# and wekan/charts, used by the website + charts jobs to push
|
||||
# to those repos (the built-in GITHUB_TOKEN only covers
|
||||
# wekan/wekan). See releases/create-github-secrets.sh.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
old_version:
|
||||
description: 'Previous version number (e.g. 8.42, without v prefix)'
|
||||
description: 'Previous version number (e.g. 9.35, without v prefix)'
|
||||
required: true
|
||||
type: string
|
||||
new_version:
|
||||
description: 'New version number (e.g. 8.43, without v prefix)'
|
||||
description: 'New version number (e.g. 9.36, without v prefix)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Node.js major version that Meteor 3.5-beta bundles. Used for native module
|
||||
# rebuilds on every platform — it must match the amd64 build's Node ABI.
|
||||
env:
|
||||
NODE_VERSION: '24'
|
||||
|
||||
jobs:
|
||||
|
||||
# ─── 1. Extract version and release notes ───────────────────────────────────
|
||||
# ─── 0. Bump versions + rebuild API docs, commit to main ────────────────────
|
||||
# Runs releases/version.sh (probes upstream for the newest Node.js/MongoDB,
|
||||
# bumps every version reference) and releases/rebuild-docs.sh (regenerates
|
||||
# public/api/wekan.yml + wekan.html), then commits and pushes to main so the
|
||||
# tag, builds, website and charts jobs all see the bumped sources.
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install version-bump and docs build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 python3-pip curl wget
|
||||
|
||||
- name: Bump versions and rebuild API docs
|
||||
env:
|
||||
# Builds fetch dependencies from upstream themselves, so skip the slow
|
||||
# pre-download of Node/MongoDB tarballs that the local flow does.
|
||||
RELEASE_SKIP_DEP_DOWNLOAD: '1'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
./releases/version.sh "${{ inputs.old_version }}" "${{ inputs.new_version }}"
|
||||
./releases/rebuild-docs.sh "${{ inputs.new_version }}"
|
||||
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
git add --all
|
||||
if git diff --cached --quiet; then
|
||||
echo "No version changes to commit."
|
||||
else
|
||||
git commit -m "Bump versions for v${{ inputs.new_version }}"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
# ─── 1. Tag the release and extract release notes ───────────────────────────
|
||||
prepare:
|
||||
needs: bump
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
@@ -42,29 +117,42 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set version from input
|
||||
- name: Normalise version from input
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
VERSION="${{ inputs.new_version }}"
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create and push tag v${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag v$VERSION already exists, leaving it as is."
|
||||
else
|
||||
git tag -a "v$VERSION" -m "v$VERSION"
|
||||
git push origin "v$VERSION"
|
||||
fi
|
||||
|
||||
- name: Extract release notes from CHANGELOG.md
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
# Try to find the version-specific section first, then fall back to Upcoming
|
||||
# Prefer a version-specific "# v<version> ..." section, otherwise fall
|
||||
# back to the topmost "# Upcoming/Upcomig WeKan ® release" section.
|
||||
NOTES=$(python3 - <<PYEOF
|
||||
import re, sys
|
||||
version = "$VERSION"
|
||||
with open("CHANGELOG.md", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
# Pattern: section heading through (but not including) the next heading
|
||||
patterns = [
|
||||
rf"(# v{re.escape(version)} .*?)(?=\n# v[0-9]|\Z)",
|
||||
r"(# Upcoming WeKan.*?)(?=\n# v[0-9]|\Z)",
|
||||
r"(# Upcom\w* WeKan.*?)(?=\n# v[0-9]|\Z)",
|
||||
]
|
||||
for pat in patterns:
|
||||
m = re.search(pat, content, re.DOTALL)
|
||||
@@ -74,28 +162,98 @@ jobs:
|
||||
print(f"WeKan v{version}")
|
||||
PYEOF
|
||||
)
|
||||
echo "content<<GHEOF" >> $GITHUB_OUTPUT
|
||||
echo "$NOTES" >> $GITHUB_OUTPUT
|
||||
echo "GHEOF" >> $GITHUB_OUTPUT
|
||||
{
|
||||
echo "content<<GHEOF"
|
||||
echo "$NOTES"
|
||||
echo "GHEOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ─── 1b. Update the wekan.fi website repo (parallel) ────────────────────────
|
||||
# Bumps the version/Meteor/Node/MongoDB strings on the install page and copies
|
||||
# the freshly rebuilt API docs into wekan.fi/api/v<new>, then commits + pushes
|
||||
# to wekan/wekan.fi. Runs as soon as `bump` has pushed the bumped sources.
|
||||
website:
|
||||
needs: bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wekan repo (bumped main, with rebuilt API docs)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
path: wekan
|
||||
|
||||
- name: Checkout wekan.fi website repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: wekan/wekan.fi
|
||||
token: ${{ secrets.WEKAN_REPO_TOKEN }}
|
||||
path: wekan.fi
|
||||
|
||||
- name: Update and push website
|
||||
env:
|
||||
WEBDIR: ${{ github.workspace }}/wekan.fi
|
||||
WEKANREPODIR: ${{ github.workspace }}/wekan
|
||||
RELEASE_PUSH_WEBSITE: '1'
|
||||
run: |
|
||||
cd wekan.fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
cd ..
|
||||
./wekan/releases/release-website.sh "${{ inputs.old_version }}" "${{ inputs.new_version }}"
|
||||
|
||||
# ─── 1c. Update and publish the Helm chart repo (parallel) ──────────────────
|
||||
# Bumps the WeKan version in wekan/charts, packages the chart and publishes it
|
||||
# to the gh-pages Helm index, then pushes both branches. Runs in parallel with
|
||||
# the platform builds; packaging the chart does not need the built artifacts.
|
||||
charts:
|
||||
needs: bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wekan repo (for releases/release-charts.sh + version refs)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
path: wekan
|
||||
|
||||
- name: Checkout wekan/charts repo (main + gh-pages)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: wekan/charts
|
||||
token: ${{ secrets.WEKAN_REPO_TOKEN }}
|
||||
fetch-depth: 0
|
||||
path: charts
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Fetch gh-pages branch (release-charts.sh switches to it)
|
||||
run: |
|
||||
cd charts
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git fetch origin '+refs/heads/*:refs/remotes/origin/*'
|
||||
git branch --track gh-pages origin/gh-pages 2>/dev/null || true
|
||||
|
||||
- name: Release and publish chart
|
||||
env:
|
||||
CHARTS_DIR: ${{ github.workspace }}/charts
|
||||
run: |
|
||||
./wekan/releases/release-charts.sh "${{ inputs.new_version }}"
|
||||
|
||||
# ─── 2. Build amd64 bundle (full Meteor 3 build) ────────────────────────────
|
||||
# Meteor 3.5-beta.7 installer supports only: Linux x86_64, Linux aarch64, macOS, Windows.
|
||||
# It explicitly rejects armhf, s390x, and ppc64le at install time.
|
||||
# Therefore the full Meteor build always runs here on x86_64.
|
||||
# All other Linux architectures then rebuild only the native npm modules
|
||||
# from this bundle (much faster than a full Meteor build).
|
||||
# Node.js version must match what Meteor 3.5-beta.7 bundles (detected automatically).
|
||||
# Meteor 3.5-beta installer supports only Linux x86_64/aarch64, macOS, Windows.
|
||||
# The full Meteor build always runs here on x86_64; every other platform then
|
||||
# only rebuilds the native npm modules from this bundle.
|
||||
build-amd64:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ inputs.new_version }}
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
@@ -115,11 +273,9 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip
|
||||
|
||||
# Pin to detected latest Node.js 24.x — must match Meteor's bundled version.
|
||||
# Using the same version for build and npm rebuild ensures consistent native module ABI.
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '${{ needs.prepare.outputs.version }}'
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Meteor
|
||||
run: |
|
||||
@@ -134,6 +290,11 @@ jobs:
|
||||
(meteor update --npm 2>/dev/null || true) && meteor npm install
|
||||
meteor build .build --directory
|
||||
|
||||
- name: Install server npm modules
|
||||
run: |
|
||||
cd .build/bundle/programs/server
|
||||
npm install
|
||||
|
||||
- name: Create amd64 zip
|
||||
run: |
|
||||
cd .build
|
||||
@@ -146,30 +307,18 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
# ─── 3. Build arm64 bundle (rebuild native modules natively) ─────────────────
|
||||
# Meteor 3.5-beta.7 does support Linux aarch64, but running a full Meteor build a
|
||||
# second time would double the build time for the same result. npm rebuild on
|
||||
# the native arm64 GitHub Actions runner is much faster and produces the same
|
||||
# platform-specific native modules.
|
||||
build-arm64:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION_ARM64: ''
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
|
||||
|
||||
|
||||
- name: Set Node.js version for arm64
|
||||
id: node_arm64
|
||||
run: |
|
||||
echo "NODE_VERSION_ARM64=24" >> $GITHUB_ENV
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
@@ -192,14 +341,13 @@ jobs:
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
# Use the latest available Node.js 24.x slim image for arm64
|
||||
docker run --rm --platform linux/arm64 \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24-slim \
|
||||
node:${NODE_VERSION}-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Clean up and create arm64 zip
|
||||
- name: Create arm64 zip
|
||||
run: |
|
||||
zip -r wekan-${VERSION}-arm64.zip bundle
|
||||
|
||||
@@ -209,195 +357,6 @@ jobs:
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-arm64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 4. Build s390x bundle (rebuild native modules via QEMU container) ───────
|
||||
# Meteor 3.5-beta.7 does NOT support s390x (installer exits with error on that arch).
|
||||
# GitHub Actions has no native s390x runners.
|
||||
# Solution: QEMU emulation + node:<version>-slim (s390x variant) for npm rebuild.
|
||||
build-s390x:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION_S390X: ''
|
||||
steps:
|
||||
- name: Set up QEMU for s390x
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: s390x
|
||||
|
||||
- name: Install unzip/zip
|
||||
run: sudo apt-get update && sudo apt-get install -y unzip zip
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Set Node.js version for s390x
|
||||
id: node_s390x
|
||||
run: |
|
||||
echo "NODE_VERSION_S390X=24" >> $GITHUB_ENV
|
||||
|
||||
- name: Rebuild native modules for s390x in container
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
rm -rf bundle/programs/server/node_modules
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
if [ -d "$BCRYPT_DIR" ]; then
|
||||
rm -rf "$BCRYPT_DIR"
|
||||
TMP=$(mktemp -d)
|
||||
pushd "$TMP" > /dev/null
|
||||
npm pack bcrypt@6.0.0
|
||||
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
|
||||
cd bcrypt && npm install --ignore-scripts && cd ..
|
||||
cp -a bcrypt "$BCRYPT_DIR"
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
# Use the latest available Node.js 24.x slim image for s390x
|
||||
docker run --rm --platform linux/s390x \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Clean up and create s390x zip
|
||||
run: |
|
||||
zip -r wekan-${VERSION}-s390x.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-s390x
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-s390x.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 4b. Build ppc64le bundle (rebuild native modules via QEMU container) ────
|
||||
# Meteor 3.5-beta.7 does NOT support ppc64le (installer exits with error on that arch).
|
||||
# GitHub Actions has no native ppc64le runners.
|
||||
# Solution: QEMU emulation + node:<version>-slim (ppc64le variant) for npm rebuild.
|
||||
build-ppc64le:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION_PPC64LE: ''
|
||||
steps:
|
||||
- name: Set up QEMU for ppc64le
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: ppc64le
|
||||
|
||||
- name: Install unzip/zip
|
||||
run: sudo apt-get update && sudo apt-get install -y unzip zip
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Set Node.js version for ppc64le
|
||||
id: node_ppc64le
|
||||
run: |
|
||||
echo "NODE_VERSION_PPC64LE=24" >> $GITHUB_ENV
|
||||
|
||||
- name: Rebuild native modules for ppc64le in container
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
rm -rf bundle/programs/server/node_modules
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
if [ -d "$BCRYPT_DIR" ]; then
|
||||
rm -rf "$BCRYPT_DIR"
|
||||
TMP=$(mktemp -d)
|
||||
pushd "$TMP" > /dev/null
|
||||
npm pack bcrypt@6.0.0
|
||||
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
|
||||
cd bcrypt && npm install --ignore-scripts && cd ..
|
||||
cp -a bcrypt "$BCRYPT_DIR"
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
# Use the latest available Node.js 24.x slim image for ppc64le
|
||||
docker run --rm --platform linux/ppc64le \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Clean up and create ppc64le zip
|
||||
run: |
|
||||
zip -r wekan-${VERSION}-ppc64le.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-ppc64le
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 4c. Build armhf bundle — COMMENTED OUT ─────────────────────────────────
|
||||
# No official Node.js binaries exist for armhf (arm/v7).
|
||||
#build-armhf:
|
||||
# needs: [prepare, build-amd64]
|
||||
# runs-on: ubuntu-latest
|
||||
# timeout-minutes: 120
|
||||
# env:
|
||||
# VERSION: ${{ needs.prepare.outputs.version }}
|
||||
# steps:
|
||||
# - name: Set up QEMU for arm/v7
|
||||
# uses: docker/setup-qemu-action@v4
|
||||
# with:
|
||||
# platforms: arm
|
||||
#
|
||||
# - name: Install unzip/zip
|
||||
# run: sudo apt-get update && sudo apt-get install -y unzip zip
|
||||
#
|
||||
# - name: Download amd64 bundle
|
||||
# uses: actions/download-artifact@v8
|
||||
# with:
|
||||
# name: bundle-amd64
|
||||
#
|
||||
# - name: Rebuild native modules for armhf in container
|
||||
# run: |
|
||||
# unzip wekan-${VERSION}-amd64.zip
|
||||
# rm -rf bundle/programs/server/node_modules
|
||||
# # Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# # which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
# BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
# if [ -d "$BCRYPT_DIR" ]; then
|
||||
# rm -rf "$BCRYPT_DIR"
|
||||
# TMP=$(mktemp -d)
|
||||
# pushd "$TMP" > /dev/null
|
||||
# npm pack bcrypt@6.0.0
|
||||
# mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
|
||||
# cd bcrypt && npm install --ignore-scripts && cd ..
|
||||
# cp -a bcrypt "$BCRYPT_DIR"
|
||||
# popd > /dev/null
|
||||
# rm -rf "$TMP"
|
||||
# fi
|
||||
# # node:<version>-slim arm/v7 — same Node.js version as Meteor bundled
|
||||
# docker run --rm --platform linux/arm/v7 \
|
||||
# -v "$(pwd)/bundle:/bundle" \
|
||||
# -w /bundle/programs/server \
|
||||
# node:${NODE_VERSION}-slim \
|
||||
# sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
#
|
||||
# - name: Clean up and create armhf zip
|
||||
# run: |
|
||||
# zip -r wekan-${VERSION}-armhf.zip bundle
|
||||
#
|
||||
# - uses: actions/upload-artifact@v7
|
||||
# with:
|
||||
# name: bundle-armhf
|
||||
# path: wekan-${{ needs.prepare.outputs.version }}-armhf.zip
|
||||
# retention-days: 1
|
||||
|
||||
# ─── 5. Build win64 bundle (rebuild native modules on Windows) ───────────────
|
||||
build-win64:
|
||||
needs: [prepare, build-amd64]
|
||||
@@ -406,14 +365,17 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
# Checked out into src/ so start-wekan.bat and snapcraft.yaml (which pins
|
||||
# the MongoDB tools/mongosh versions) are available; the downloaded bundle
|
||||
# is extracted alongside it in the workspace root.
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
path: src
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '${{ needs.prepare.outputs.version }}'
|
||||
|
||||
## With Node 24 these are already preinstalled at GitHub Windows runners
|
||||
#- name: Install node-gyp and build tools
|
||||
# run: |
|
||||
# npm install -g windows-build-tools --vs2019 2>$null; exit 0
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install 7-zip
|
||||
run: choco install -y 7zip
|
||||
@@ -443,12 +405,45 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Rebuild native modules for Windows
|
||||
shell: cmd
|
||||
shell: bash
|
||||
run: |
|
||||
cd bundle\programs\server
|
||||
rmdir /s /q node_modules
|
||||
cd bundle/programs/server
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
cd ..\..\..
|
||||
|
||||
- name: Add MongoDB tools, mongosh and start-wekan.bat to the bundle
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Use the same MongoDB tools/mongosh versions as the snap, which
|
||||
# releases/version.sh keeps at the newest release for each bundle.
|
||||
TOOLS_VER=$(grep -oE 'mongodb-database-tools-ubuntu2204-\$\{TOOLS_ARCH\}-[0-9]+\.[0-9]+\.[0-9]+' src/snapcraft.yaml \
|
||||
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
MONGOSH_VER=$(grep -oE 'mongosh-[0-9]+\.[0-9]+\.[0-9]+-linux' src/snapcraft.yaml \
|
||||
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
echo "MongoDB Database Tools: $TOOLS_VER mongosh: $MONGOSH_VER"
|
||||
|
||||
# MongoDB Database Tools (Windows): bsondump, mongodump, mongoexport,
|
||||
# mongofiles, mongoimport, mongorestore, mongostat, mongotop.
|
||||
TOOLS_DIR="mongodb-database-tools-windows-x86_64-${TOOLS_VER}"
|
||||
curl -fL -o tools.zip \
|
||||
"https://fastdl.mongodb.org/tools/db/${TOOLS_DIR}.zip"
|
||||
7z x -y tools.zip > /dev/null
|
||||
cp "${TOOLS_DIR}/bin/"*.exe bundle/
|
||||
|
||||
# mongosh (Windows): mongosh.exe + mongosh_crypt_v1.dll.
|
||||
MONGOSH_DIR="mongosh-${MONGOSH_VER}-win32-x64"
|
||||
curl -fL -o mongosh.zip \
|
||||
"https://downloads.mongodb.com/compass/${MONGOSH_DIR}.zip"
|
||||
7z x -y mongosh.zip > /dev/null
|
||||
cp "${MONGOSH_DIR}/bin/mongosh.exe" bundle/
|
||||
cp "${MONGOSH_DIR}/bin/mongosh_crypt_v1.dll" bundle/
|
||||
|
||||
# Windows launcher from the repo root.
|
||||
cp src/start-wekan.bat bundle/
|
||||
|
||||
echo "=== bundle contents ==="
|
||||
ls -1 bundle/
|
||||
|
||||
- name: Create win64 zip
|
||||
shell: cmd
|
||||
@@ -460,9 +455,62 @@ jobs:
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-win64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 6. Create GitHub Release and upload all bundles ────────────────────────
|
||||
# ─── 6. Build mac-arm64 bundle (rebuild native modules on Apple Silicon) ─────
|
||||
build-mac-arm64:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: macos-14 # macos-14 runners are Apple Silicon (arm64)
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Rebuild native modules for macOS arm64
|
||||
run: |
|
||||
unzip -q wekan-${VERSION}-amd64.zip
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
if [ -d "$BCRYPT_DIR" ]; then
|
||||
rm -rf "$BCRYPT_DIR"
|
||||
TMP=$(mktemp -d)
|
||||
pushd "$TMP" > /dev/null
|
||||
npm pack bcrypt@6.0.0
|
||||
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
|
||||
cd bcrypt && npm install --ignore-scripts && cd ..
|
||||
cp -a bcrypt "$BCRYPT_DIR"
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
cd bundle/programs/server
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
cd ../../..
|
||||
|
||||
- name: Create mac-arm64 zip
|
||||
run: zip -r wekan-${VERSION}-mac-arm64.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-mac-arm64
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-mac-arm64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 7. Create GitHub Release and upload all bundles ────────────────────────
|
||||
release:
|
||||
needs: [prepare, build-amd64, build-arm64, build-s390x, build-ppc64le, build-win64]
|
||||
needs:
|
||||
- prepare
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
- build-win64
|
||||
- build-mac-arm64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
@@ -482,6 +530,104 @@ jobs:
|
||||
files: |
|
||||
wekan-${{ needs.prepare.outputs.version }}-amd64.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-arm64.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-s390x.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-win64.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-mac-arm64.zip
|
||||
|
||||
# ─── 8. Build and push the multi-arch Docker image to all registries ────────
|
||||
# Mirrors releases/docker-build.sh step 3 (multi-arch buildx --push).
|
||||
# Needs `release` because the Dockerfile downloads wekan-<version>-<arch>.zip
|
||||
# from the GitHub Release; building before those assets exist 404s and (without
|
||||
# set -e in the Dockerfile) shipped a broken image missing /build/main.js.
|
||||
docker:
|
||||
needs: [prepare, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub, Quay.io and GHCR
|
||||
env:
|
||||
DOCKERHUB_AUTH: ${{ secrets.DOCKERHUB_AUTH }}
|
||||
QUAY_AUTH: ${{ secrets.QUAY_AUTH }}
|
||||
GHCR_AUTH: ${{ secrets.GHCR_AUTH }}
|
||||
run: |
|
||||
login() {
|
||||
# $1 = registry host, $2 = base64("user:token")
|
||||
local host="$1" creds user token
|
||||
creds="$(printf '%s' "$2" | base64 -d)"
|
||||
user="${creds%%:*}"
|
||||
token="${creds#*:}"
|
||||
printf '%s' "$token" | docker login "$host" -u "$user" --password-stdin
|
||||
}
|
||||
login docker.io "$DOCKERHUB_AUTH"
|
||||
login quay.io "$QUAY_AUTH"
|
||||
login ghcr.io "$GHCR_AUTH"
|
||||
|
||||
- name: Build and push multi-arch image
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t wekanteam/wekan:v${VERSION} \
|
||||
-t wekanteam/wekan:latest \
|
||||
-t quay.io/wekan/wekan:v${VERSION} \
|
||||
-t quay.io/wekan/wekan:latest \
|
||||
-t ghcr.io/wekan/wekan:v${VERSION} \
|
||||
-t ghcr.io/wekan/wekan:latest \
|
||||
--push \
|
||||
.
|
||||
|
||||
# ─── 9. Build the snap on Launchpad, push to the store, attach to release ────
|
||||
# The snap's `wekan` part downloads wekan-<version>-<arch>.zip from the GitHub
|
||||
# Release, so this job runs only after `release` has published those assets.
|
||||
# `snapcraft remote-build` builds every platform listed in snapcraft.yaml
|
||||
# (amd64 + arm64) on Launchpad.
|
||||
snap:
|
||||
needs: [prepare, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
# remote-build (Launchpad) rejects shallow clones, so fetch full history
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install snapcraft
|
||||
run: sudo snap install snapcraft --classic
|
||||
|
||||
- name: Provide Launchpad credentials for remote-build
|
||||
env:
|
||||
LP_CREDENTIALS: ${{ secrets.LP_CREDENTIALS }}
|
||||
run: |
|
||||
mkdir -p ~/.local/share/snapcraft
|
||||
printf '%s' "$LP_CREDENTIALS" | base64 -d > ~/.local/share/snapcraft/launchpad-credentials
|
||||
chmod 600 ~/.local/share/snapcraft/launchpad-credentials
|
||||
|
||||
- name: Build snaps on Launchpad (amd64 + arm64)
|
||||
run: |
|
||||
snapcraft remote-build --launchpad-accept-public-upload
|
||||
|
||||
- name: Push snaps to the Snap Store (edge + beta channels)
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
|
||||
run: |
|
||||
for f in wekan_${VERSION}_*.snap; do
|
||||
echo "Uploading $f to the Snap Store (edge,beta)..."
|
||||
snapcraft upload --release=edge,beta "$f"
|
||||
done
|
||||
|
||||
- name: Attach snaps to the GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload "v${VERSION}" wekan_${VERSION}_*.snap --clobber
|
||||
+7
-7
@@ -16,11 +16,11 @@ blaze@3.0.2
|
||||
aldeed:collection2
|
||||
aldeed:schema-index@4.0.0-beta.5
|
||||
reywood:publish-composite
|
||||
mongo@2.4.0-beta350.12
|
||||
mongo@2.4.0-rc350.1
|
||||
|
||||
# Account system
|
||||
accounts-password@3.3.0-beta350.12
|
||||
accounts-express
|
||||
accounts-password@3.3.0-rc350.1
|
||||
accounts-express@1.0.0-rc350.1
|
||||
communitypackages:core
|
||||
communitypackages:unstyled
|
||||
communitypackages:flow-routing
|
||||
@@ -37,7 +37,7 @@ wekan-accounts-oidc
|
||||
check@1.5.0
|
||||
random@1.2.2
|
||||
reactive-dict@1.3.2
|
||||
session@1.2.3-beta350.12
|
||||
session@1.2.3-rc350.1
|
||||
tracker@1.3.4
|
||||
audit-argument-checks@1.0.8
|
||||
|
||||
@@ -45,7 +45,7 @@ audit-argument-checks@1.0.8
|
||||
ostrio:i18n
|
||||
reactive-var@1.0.13
|
||||
shell-server@0.7.0
|
||||
email@3.2.0-beta350.12
|
||||
email@3.2.0-rc350.1
|
||||
dynamic-import@0.7.4
|
||||
wekan-markdown
|
||||
quave:synced-cron
|
||||
@@ -55,8 +55,8 @@ ostrio:files
|
||||
meteortesting:mocha@3.0.0
|
||||
matb33:collection-hooks
|
||||
service-configuration@1.3.5
|
||||
minifier-css@2.0.2-beta350.12
|
||||
ejson@1.2.0-beta350.12
|
||||
minifier-css@2.0.2-rc350.1
|
||||
ejson@1.2.0-rc350.1
|
||||
logging@1.3.6
|
||||
wekan-fullcalendar
|
||||
wekan-fontawesome
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
METEOR@3.5-beta.12
|
||||
METEOR@3.5-rc.1
|
||||
+25
-25
@@ -1,8 +1,8 @@
|
||||
accounts-base@3.3.0-beta350.12
|
||||
accounts-oauth@1.4.7-beta350.12
|
||||
accounts-password@3.3.0-beta350.12
|
||||
accounts-express@1.0.0-beta350.12
|
||||
aldeed:collection2@4.1.5
|
||||
accounts-base@3.3.0-rc350.1
|
||||
accounts-express@1.0.0-rc350.1
|
||||
accounts-oauth@1.4.7-rc350.1
|
||||
accounts-password@3.3.0-rc350.1
|
||||
aldeed:collection2@4.2.0
|
||||
aldeed:schema-index@4.0.0
|
||||
aldeed:simple-schema@1.13.1
|
||||
allow-deny@2.1.0
|
||||
@@ -10,35 +10,35 @@ audit-argument-checks@1.0.8
|
||||
autoupdate@2.0.1
|
||||
babel-compiler@7.14.0
|
||||
babel-runtime@1.5.2
|
||||
base64@1.0.14-beta350.12
|
||||
binary-heap@1.0.13-beta350.12
|
||||
base64@1.0.14-rc350.1
|
||||
binary-heap@1.0.13-rc350.1
|
||||
blaze@3.0.3
|
||||
blaze-tools@2.0.1
|
||||
boilerplate-generator@2.1.0
|
||||
caching-compiler@2.0.1
|
||||
caching-html-compiler@2.0.1
|
||||
callback-hook@1.8.0-beta350.12
|
||||
callback-hook@1.8.0-rc350.1
|
||||
check@1.5.0
|
||||
communitypackages:core@2.0.3
|
||||
communitypackages:flow-routing@2.1.0
|
||||
communitypackages:unstyled@2.0.1
|
||||
core-runtime@1.0.0
|
||||
ddp@1.4.2
|
||||
ddp-client@3.3.0-beta350.12
|
||||
ddp-common@1.4.5-beta350.12
|
||||
ddp-rate-limiter@1.3.0-beta350.12
|
||||
ddp-server@3.3.0-beta350.12
|
||||
ddp-client@3.3.0-rc350.1
|
||||
ddp-common@1.4.5-rc350.1
|
||||
ddp-rate-limiter@1.3.0-rc350.1
|
||||
ddp-server@3.3.0-rc350.1
|
||||
diff-sequence@1.1.3
|
||||
dynamic-import@0.7.4
|
||||
ecmascript@0.18.0
|
||||
ecmascript-runtime@0.8.3
|
||||
ecmascript-runtime-client@0.13.0
|
||||
ecmascript-runtime-server@0.11.1
|
||||
ejson@1.2.0-beta350.12
|
||||
email@3.2.0-beta350.12
|
||||
ejson@1.2.0-rc350.1
|
||||
email@3.2.0-rc350.1
|
||||
es5-shim@4.8.1
|
||||
facts-base@1.0.2
|
||||
fetch@0.2.0-beta350.12
|
||||
fetch@0.2.0-rc350.1
|
||||
geojson-utils@1.0.12
|
||||
hot-code-push@1.0.5
|
||||
html-tools@2.0.1
|
||||
@@ -53,23 +53,23 @@ lai:collection-extensions@1.0.0
|
||||
localstorage@1.2.1
|
||||
logging@1.3.6
|
||||
matb33:collection-hooks@2.1.0
|
||||
meteor@2.3.1-beta350.12
|
||||
meteor@2.3.1-rc350.1
|
||||
meteor-base@1.5.2
|
||||
meteortesting:browser-tests@1.8.0
|
||||
meteortesting:mocha@3.3.0
|
||||
meteortesting:mocha-core@8.2.0
|
||||
minifier-css@2.0.2-beta350.12
|
||||
minifier-css@2.0.2-rc350.1
|
||||
minifier-js@3.2.0
|
||||
minimongo@2.2.0-beta350.12
|
||||
minimongo@2.2.0-rc350.1
|
||||
modern-browsers@0.2.3
|
||||
modules@0.20.3
|
||||
modules-runtime@0.13.2
|
||||
mongo@2.4.0-beta350.12
|
||||
mongo@2.4.0-rc350.1
|
||||
mongo-decimal@0.2.0
|
||||
mongo-dev-server@1.1.1
|
||||
mongo-id@1.0.9
|
||||
npm-mongo@6.16.1
|
||||
oauth@3.0.4-beta350.12
|
||||
oauth@3.0.4-rc350.1
|
||||
oauth2@1.3.3
|
||||
observe-sequence@2.0.1
|
||||
ordered-dict@1.2.0
|
||||
@@ -82,7 +82,7 @@ promise@1.0.0
|
||||
quave:synced-cron@2.4.0
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.2
|
||||
rate-limit@1.2.0-beta350.12
|
||||
rate-limit@1.2.0-rc350.1
|
||||
react-fast-refresh@0.3.0
|
||||
reactive-dict@1.3.2
|
||||
reactive-var@1.0.13
|
||||
@@ -92,10 +92,10 @@ reywood:publish-composite@1.9.0
|
||||
routepolicy@1.1.2
|
||||
rspack@1.1.0
|
||||
service-configuration@1.3.5
|
||||
session@1.2.3-beta350.12
|
||||
session@1.2.3-rc350.1
|
||||
sha@1.0.10
|
||||
shell-server@0.7.0
|
||||
socket-stream-client@0.7.0-beta350.12
|
||||
socket-stream-client@0.7.0-rc350.1
|
||||
spacebars@2.0.1
|
||||
spacebars-compiler@2.0.1
|
||||
standard-minifier-js@3.2.0
|
||||
@@ -106,8 +106,8 @@ templating-tools@2.0.1
|
||||
tools-core@1.1.0
|
||||
tracker@1.3.4
|
||||
typescript@5.10.0
|
||||
url@1.3.6-beta350.12
|
||||
webapp@2.2.0-beta350.12
|
||||
url@1.3.6-rc350.1
|
||||
webapp@2.2.0-rc350.1
|
||||
webapp-hashing@1.1.2
|
||||
wekan-accounts-cas@0.2.0
|
||||
wekan-accounts-lockout@1.1.0
|
||||
|
||||
+987
@@ -26,6 +26,993 @@ Versions:
|
||||
- WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible,
|
||||
it was fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
|
||||
|
||||
# v9.42 2026-06-13 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Greatly improved import from Trello to WeKan](https://github.com/wekan/wekan/commit/899617bd4d83905df05fd8aab1d26630e676fee5):
|
||||
- Attachments now import. Trello attachment URLs require OAuth to download, so
|
||||
WeKan could not fetch them. The Trello import page now has two separate file
|
||||
fields: a **.json** field for a single Trello board, and a
|
||||
**.zip** field for a package produced by the
|
||||
[Trello Card Attachments Downloader](https://github.com/wekan/trello-attachments-downloader).
|
||||
The .zip may contain one *or more* board .json files together with each
|
||||
board's attachments in a board-name subfolder. The .zip is uploaded over HTTP
|
||||
and processed entirely on the server (the attachment bytes never travel over
|
||||
the realtime WebSocket, which previously overflowed the connection and made
|
||||
the page flicker): WeKan detects every board .json, matches each board to its
|
||||
attachment subfolder by name, streams each file straight into the Default
|
||||
file storage, imports all boards, then opens All Boards. The upload is guarded
|
||||
against abuse — it rejects oversized uploads, zip bombs (too many files or too
|
||||
much uncompressed data, per-file size caps) and unsafe entry paths (absolute
|
||||
paths or `..` traversal), and never overwrites existing files (stored under
|
||||
generated ids). The importer also reads attachments from `card.attachments`
|
||||
(newer Trello exports), not only from the action log, and uses the correct
|
||||
Meteor-Files insert API (the previous calls were no-ops on Meteor 3). Single
|
||||
board imports (the .json file or the pasted JSON, after the map-members step)
|
||||
now run through the same server-side HTTP path rather than the realtime
|
||||
`importBoard` method, so a heavy import can no longer drop the WebSocket and
|
||||
get retried in an endless loop (the page flickering with "Invalid frame
|
||||
header" / repeated "logged out" errors). When importing only a single board
|
||||
.json and you have saved Trello API key and token, WeKan uses them on the
|
||||
server to download that board's attachments, background image, member
|
||||
avatars and card stickers from Trello (a .json export often omits stickers;
|
||||
avatars only fill in mapped users who have none — an existing avatar is never
|
||||
overwritten); each download is best-effort and a failure never aborts the
|
||||
import. After a single-board import the new board's data is subscribed and
|
||||
loaded before opening it, so it shows its swimlanes, lists and cards
|
||||
immediately instead of needing a browser page reload (the HTTP import, unlike
|
||||
the old realtime method, does not push the written documents to the client by
|
||||
itself).
|
||||
- Import Trello-only card features: stickers (shown as Font Awesome icons on
|
||||
minicards and the card detail), card cover (color or attachment), card
|
||||
location (name, address, coordinates with an OpenStreetMap link), and the
|
||||
due-date "complete" checkbox shown on the minicard. Trello's two named
|
||||
sticker packs are renamed and highlighted (the icon keeps its normal colour)
|
||||
so they stay distinct: the "taco" pack becomes "mascot" with an underlined
|
||||
icon, and the "pete" pack becomes "computer" with a ring around the icon,
|
||||
e.g. "pete-ghost" imports as "computer ghost" with a Snapchat-ghost icon.
|
||||
Several named stickers that used to share an icon now get their own Font
|
||||
Awesome 4.7 icon (mascot active => heartbeat, pixel => qrcode, proto =>
|
||||
flask, embarrassed => meh-o, clean => bath, computer shipped => truck).
|
||||
- Live Trello API import: paste your Trello API key and token on the Trello
|
||||
import page to list all your Trello workspaces and boards, then import all
|
||||
or selected boards together with their attachments (downloaded
|
||||
server-side). Imported boards are placed under a personal workspace named
|
||||
after their Trello workspace, created by name if it does not exist, under an
|
||||
optional parent workspace you choose. The API client respects Trello's rate
|
||||
limits: it spaces out requests, honours `Retry-After` on HTTP 429, retries
|
||||
transient 5xx and network errors with capped exponential backoff, and
|
||||
reports a clear error on an invalid key/token (401). After a successful API
|
||||
import you are taken to the All Boards page where the new boards appear.
|
||||
- The live API import runs as a background job on the server: you can navigate
|
||||
away and come back to the Trello import page to watch progress, see
|
||||
per-board results, and read the full error log. Errors are shown in a
|
||||
selectable text box with a one-click "Copy to clipboard" button (and you can
|
||||
still select and copy just part of the text) so they are easy to share when
|
||||
fixing issues. If the import stops on a fatal API error (invalid token, or
|
||||
rate limit still failing after retries) you can fix the cause and Resume
|
||||
from where it left off, and a server restart leaves the job paused for
|
||||
resuming rather than lost. You can also Cancel the import, or Cancel and
|
||||
delete the boards already imported, to start the whole process over cleanly.
|
||||
- You can optionally save your Trello API key and token with the Save and
|
||||
Delete buttons on the import page. When saved, they are stored only on the
|
||||
server, never sent back to the browser, and are reused automatically when
|
||||
you list workspaces or import (so you do not have to paste them again); a
|
||||
"saved" indicator is shown, Save can overwrite them with new values, and
|
||||
Delete clears them from the database. If you do not save them, they are kept
|
||||
only in server memory for the running import and re-supplied when you resume.
|
||||
- Larger Trello JSON exports can be selected as a file instead of pasted.
|
||||
- Keep imported usernames for later mapping. Imported members that match an
|
||||
existing WeKan user (by username, or by a previously recorded imported
|
||||
username) are mapped automatically, and the imported username is remembered
|
||||
on that user so future imports map too. Members with no matching WeKan user
|
||||
are no longer lost: their usernames are kept on the board, so an admin can
|
||||
map them to real users later in Admin Panel / People (Imported Usernames),
|
||||
without having to map everyone up front at import time.
|
||||
Thanks to xet7 and Claude.
|
||||
- Board background images can now be stored in WeKan. A new `backgrounds` file
|
||||
storage directory is created alongside `attachments` and `avatars` (using the
|
||||
current default storage backend), and Board menu → Board backgrounds lets a
|
||||
board admin upload background images, set one as the active board background,
|
||||
download, and delete them. Board export now includes the board's background
|
||||
images and re-imports them, and a Trello board's background image is
|
||||
downloaded and stored on import (so it keeps working even if the original
|
||||
Trello URL later changes). Thanks to xet7 and Claude.
|
||||
- Stickers can be added to and removed from cards directly in WeKan, chosen from
|
||||
a set of Font Awesome icons similar to Trello's stickers (previously stickers
|
||||
only arrived via Trello import). The picker also includes every "mascot"
|
||||
(underlined) and "computer" (ringed) highlighted sticker that a Trello import
|
||||
can produce — generated from the same icon mapping the importer uses — so any
|
||||
imported sticker can also be added by hand. Stickers show on the minicard and
|
||||
in the card detail. Thanks to xet7 and Claude.
|
||||
- The Trello-style "complete" checkbox (mark a card complete/incomplete,
|
||||
independent of the due date) is shown as an animated green checkbox to the
|
||||
left of the card title, both on the minicard and in the opened card, with
|
||||
"Mark as complete" / "Mark as incomplete" tooltips. It uses the same animated
|
||||
checkbox style as Admin Panel / Settings / Announcements and is vertically
|
||||
centered with the title text, and the two stay in sync. The minicard and the
|
||||
opened-card checkboxes use the same checked style (the board theme's colour, or
|
||||
green when no theme is set), so they always look identical. Subtask checkboxes
|
||||
and the card-detail custom-field checkbox now use the same animated checkbox as
|
||||
checklist items (they previously used static square icons), so every checkbox
|
||||
on a card animates consistently. Thanks to xet7 and Claude.
|
||||
- Cards can now have multiple locations, similar to multiple members. The card
|
||||
detail shows a Location section (after Labels and Stickers) listing each
|
||||
location with its name, address and an OpenStreetMap link, with an "Add
|
||||
location" button to add more and a button to edit or remove each one. A single
|
||||
location imported from Trello keeps working and is shown in the same list. The
|
||||
Add/Edit location popup can also detect a location from any map link (Google
|
||||
Maps, OpenStreetMap, Bing Maps, Apple Maps, or generic `?q=`/`?ll=` links):
|
||||
paste the link, press "Detect", and the latitude, longitude and (when present)
|
||||
the place name/address are filled in automatically. The popup also has an
|
||||
"Open map links at" setting (OpenStreetMap by default, or Google/Bing/Apple
|
||||
Maps) saved to the user profile, controlling which map service the location
|
||||
"Open in map" links use. Thanks to xet7 and Claude.
|
||||
- The opened card now docks to the top of the window, overlaying the global and
|
||||
board header bars, instead of opening downward from the clicked minicard, and
|
||||
it can be dragged all the way to the top without its top hiding behind those
|
||||
bars. (At 100% zoom the board wrapper no longer sets a `transform: scale(1)`,
|
||||
which had made it the containing block for the fixed card and trapped it below
|
||||
the headers.) Thanks to xet7 and Claude.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Wrong card number after Import](https://github.com/wekan/wekan/commit/aabcaa658edc2135dfd035bedea186ddf6b26068).
|
||||
Thanks to titver968, xet7 and Claude.
|
||||
- [Fix Board Export/Import error](https://github.com/wekan/wekan/commit/68e0032c6d3b23a195b4051e2853465965e073dc).
|
||||
Fix import/clone of inconsistent board JSON so the newest WeKan can import
|
||||
board exports from any newer or older WeKan version. A syntactically valid
|
||||
export can still be internally inconsistent (a board member whose user
|
||||
account was deleted is no longer in the export's `users`, cards pointing at a
|
||||
missing `listId`, orphaned checklists, or a missing `lists` array). Such
|
||||
exports previously failed import as "error-json-malformed" or silently
|
||||
dropped data (cards with an undefined list never rendered). The importer now
|
||||
skips dangling user references in the member mapper (client
|
||||
`wekanMembersMapper`, server `wekanmapper` used by board cloning, and
|
||||
`WekanCreator`), falls back to the first imported list — creating one default
|
||||
list when the export has none — for cards with a dangling `listId`, and skips
|
||||
orphaned checklists whose card is missing. The importer remains
|
||||
version-agnostic: it never reads `_format`, normalizes old `id` vs `_id`
|
||||
fields, and ignores the source `boardId`. Added `tests/wekanCreator.inconsistent.test.js`
|
||||
covering these cases.
|
||||
Thanks to titver968, xet7 and Claude.
|
||||
- Fix Trello import returning HTTP 500 on any card that has an attachment. A
|
||||
past refactor removed the `links` variable declaration but left its consumer
|
||||
in `models/trelloCreator.js`, so importing an attachment card threw
|
||||
"ReferenceError: links is not defined". Also fixed a stray `return` that
|
||||
silently aborted importing the rest of the cards when one attachment URL was
|
||||
blocked (now `continue`), and Trello cards no longer all import as card
|
||||
number #0 (the Trello short number `idShort` is preserved).
|
||||
Thanks to xet7 and Claude.
|
||||
- Fix attachments not importing from WeKan board JSON exports. The importer
|
||||
called the old Meteor-Files `Attachments.load/insert(..., cb, true)` API,
|
||||
which is a no-op on Meteor 3, so exported base64 attachments were silently
|
||||
dropped. It now inserts them with `Attachments.insertAsync`, and one failed
|
||||
attachment no longer aborts importing the rest of the cards.
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.41 2026-06-12 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Add pagination and search to Admin Panel / Reports. Fix pagination at Admin Panel / Organizations and Teams](https://github.com/wekan/wekan/commit/a12b3f2cb21a067f80269be2a5080ca91046707b).
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.40 2026-06-11 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix releasing Helm Charts](https://github.com/wekan/wekan/commit/21d18bddfb72543a812b516473a80ccf21dfda46).
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.39 2026-06-11 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix building Docker image](https://github.com/wekan/wekan/commit/8c0681d4cf020ff453959cf7ac8cb74e0a69f128).
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.38 2026-06-11 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix Docker image is broken](https://github.com/wekan/wekan/commit/108c33ab23d05ac586531c22e1fe4c4180546be9).
|
||||
Thanks to titver968, planet-goldfish, xet7 and Claude.
|
||||
- [Fix Admin Panel/People View is broken](https://github.com/wekan/wekan/commit/bcf55bdc3a804e5fae34850c0fcbf03e48c2f466).
|
||||
Thanks to titver968, xet7 and Claude.
|
||||
- [Fix Incorrect link in invite email](https://github.com/wekan/wekan/commit/5844e20d5534d04dfefc4a965d93c55765bfc08e).
|
||||
Thanks to sbaecker, xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.37 2026-06-11 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [BoardBleed](https://wekan.fi/hall-of-fame/boardbleed/):
|
||||
|
||||
- [Fixed BoardBleed](https://github.com/wekan/wekan/commit/d369a3614a4737c29d48a6345a790edf2506ddae):
|
||||
Broken access control lets any authenticated user move their Cards/Lists/Swimlanes into
|
||||
a private board they are not a member of (cross-board write via collection
|
||||
allow rule)](https://github.com/wekan/wekan/security/advisories/GHSA-gm7v-pc38-53jr)
|
||||
(CWE-284, CWE-639). WeKan boards are membership-scoped, but the DDP collection write
|
||||
policies for Cards, Lists and Swimlanes (`server/permissions/cards.js`,
|
||||
`server/permissions/lists.js`, `server/permissions/swimlanes.js`) authorized an update by
|
||||
checking only the CURRENT (pre-update) `boardId` of the document — i.e. the attacker's own
|
||||
source board — and never validated the NEW `boardId` supplied in the update modifier.
|
||||
Because every logged-in user can create a board where they are admin, an attacker could take
|
||||
a document they own and, in a single `/cards/update`, `/lists/update` or `/swimlanes/update`
|
||||
DDP call, `$set` its `boardId` (plus `swimlaneId`/`listId`) to a victim's private board: the
|
||||
allow rule saw the attacker's own source board, approved the write, and the document was
|
||||
relocated into a board the attacker is not a member of and cannot even read. This let an
|
||||
unprivileged user inject arbitrary cards/lists/swimlanes (attacker-controlled titles,
|
||||
descriptions, assignees, etc.) into any private board by id, defeating board-level access
|
||||
control. The REST API for the same operation
|
||||
(`PUT /api/boards/:boardId/lists/:listId/cards/:cardId` with `newBoardId`) was not affected
|
||||
because it correctly calls `Authentication.checkBoardWriteAccess(req.userId, newBoardId)` on
|
||||
the destination board; only the DDP allow/deny layer was vulnerable. Fixed by adding a
|
||||
`denyCrossBoardMove` helper in `server/lib/utils.js` and a `Cards.deny`/`Lists.deny`/
|
||||
`Swimlanes.deny` `update` rule on each collection that rejects any update whose modifier
|
||||
`$set`s a `boardId` on which the caller does not have write access, so a cross-board move is
|
||||
only allowed into a destination board where the user is an active write-capable member.
|
||||
Affected Wekan v9.35 and earlier. Thanks to 0xzap, xet7 and Claude.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Update release website script version numbering](https://github.com/wekan/wekan/commit/39d84f8fe893f9d64c1424730e48c2ed00193e97).
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.36 2026-06-10 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [TokenBleed](https://wekan.fi/hall-of-fame/tokenbleed/):
|
||||
|
||||
- [Fixed TokenBleed: unauthenticated login-token minting via un-awaited auth check in `POST /api/createtoken/:userId`](https://github.com/wekan/wekan/commit/08ae61161cd9602f79f441e3922ffe890b3f11de)
|
||||
(CWE-863, CWE-287). `Authentication.checkUserId` in `server/authentication.js` is an
|
||||
`async` function, so its 401 (undefined `userId`) and 403 (not an admin) throws become
|
||||
rejected promises rather than synchronous exceptions. The REST handlers in
|
||||
`server/models/users.js` and `server/models/boards.js` called it without `await` inside a
|
||||
plain synchronous `try/catch`, which cannot catch a rejected promise, so the failed check
|
||||
never stopped execution. `POST /api/createtoken/:userId` then went on to mint and return a
|
||||
usable login token for any user ID in the URL — including an admin — with no credentials at
|
||||
all (unauthenticated account takeover). The same detached-rejection bypass also affected
|
||||
`GET /api/users`, `GET /api/users/:userId`, `PUT /api/users/:userId`, `POST /api/users/`,
|
||||
`DELETE /api/users/:userId`, `POST /api/deletetoken`, `GET /api/boards`,
|
||||
`GET /api/boards_count`, `DELETE /api/boards/:boardId`, `GET /api/users/:userId/boards` and
|
||||
`POST /api/boards/:boardId/copy`. Fixed by awaiting every async `Authentication` check (and
|
||||
making the two non-`async` handlers `async`) so a failed check rejects before any privileged
|
||||
code runs. The same un-awaited pattern in the board/card/Excel/PDF export handlers
|
||||
(`models/export.js`, `models/exportExcel.js`, `models/exportExcelCard.js`, `models/exportPDF.js`,
|
||||
which were backstopped by `exporter.canExport()`) and in the checklist-create handler
|
||||
(`server/models/checklists.js`) was given the same await pass. Affected Wekan v9.35 and earlier.
|
||||
Thanks to Zion Boggan, xet7 and Claude.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Improved Security Advisory process](https://github.com/wekan/wekan/commit/f1c2f1f40f21b9f467cfe7265d22bc98fb401b57).
|
||||
Thanks to xet7.
|
||||
- [Improved Contributing](https://github.com/wekan/wekan/commit/26a03f4845b9c81902f3f955d79287a4bc61d3c4).
|
||||
Thanks to xet7.
|
||||
- [Improved Code of Conduct](https://github.com/wekan/wekan/commit/d1c71d0526eb8890fc0d8b875d94ab96f094a8c3).
|
||||
Thanks to xet7.
|
||||
- Update release scripts, trying to fix GitHub Actions builds.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/8f59d76a4e38429b434dc9e4be66a30ac1dc156e),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/5357bb47c3cf09f60a9838aadb5d21e3895c4f39).
|
||||
Thanks to xet7 and Claude.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/447877733b40a085e347a9fd12ee09d8cb263d43).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Admin Panel / Reports icons and spacing](https://github.com/wekan/wekan/commit/77d60b21a1912cda81a6fa96449a8dd99761d5d7).
|
||||
Thanks to xet7.
|
||||
- [Fix MultiSelect, so that it's possible to click select checkbox or minicard to select](https://github.com/wekan/wekan/commit/9cc066041040074befcf3dea49e7ef37b22816cc).
|
||||
Thanks to xet7 and Claude.
|
||||
- [Fix LDAP_SYNC_ADMIN_GROUPS](https://github.com/wekan/wekan/commit/3fe8f602c8c5ade128f5d7e439bff47f51a28045).
|
||||
so that admin status sync and group/role sync no longer require
|
||||
LDAP_GROUP_FILTER_ENABLE=true, which only controls the login restriction filter
|
||||
Thanks to xet7 and Claude.
|
||||
- [Expand and fix REST API](https://github.com/wekan/wekan/commit/d5f6aa196606f229c2c1871a56fb0549fbdab40e)
|
||||
(Admin API, board member, card field, card copy/move).
|
||||
REST routes live in `server/models/*.js` (using `WebApp.handlers.get/post/put/delete`),
|
||||
their schemas in `models/*.js`, OpenAPI docs are generated by `openapi/generate_openapi.py`
|
||||
into `public/api/wekan.yml`, and `api.py` is the Python CLI wrapper.
|
||||
`api.py` already gained CLI wrappers for the endpoints that exist today:
|
||||
`addboardmember`, `removeboardmember`, `setboardmemberrole`, `setcardmembers`,
|
||||
`setcardassignees`, `setcarddate`, `setcardlabels`, `movecard`.
|
||||
- **Issue [#5998](https://github.com/wekan/wekan/issues/5998): Add/remove user to
|
||||
board with a role (BoardAdmin, Normal, etc).**
|
||||
Already exists server-side: `POST /api/boards/:boardId/members/:userId/add`
|
||||
and `POST /api/boards/:boardId/members/:userId/remove` (action=add/remove), and
|
||||
`POST /api/boards/:boardId/members/:memberId` to change an existing member's
|
||||
permission flags (`isAdmin`, `isCommentOnly`, `isWorker`, `isNoComments`,
|
||||
`isNormalAssignedOnly`, `isCommentAssignedOnly`, `isReadOnly`,
|
||||
`isReadAssignedOnly`). `api.py` now wraps these with a friendly `ROLE`
|
||||
name → flags mapping.
|
||||
Implemented: the API itself now also accepts a single named `role`
|
||||
(`admin`/`normal`/`commentOnly`/`worker`/`readOnly`/`normalAssignedOnly`/
|
||||
`commentAssignedOnly`/`readAssignedOnly`/`noComments`), mapped to the permission
|
||||
booleans server-side by `boardMemberRoleToFlags` (`server/lib/utils.js`). When
|
||||
`role` is present it wins over the individual flags; an unknown role returns 400.
|
||||
Members are identified by `userId` only. Wired into both
|
||||
`POST .../members/:userId/add` and `POST .../members/:memberId`.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Add/Remove board member to card as card member or card assignee.**
|
||||
Already exists via `PUT /api/boards/:boardId/lists/:listId/cards/:cardId` with
|
||||
`members` / `assignees` (array, or '' to clear). `api.py` now wraps these as
|
||||
`setcardmembers` / `setcardassignees`. These REPLACE the list.
|
||||
Implemented: merge-style endpoints to add/remove a single member or assignee
|
||||
(`POST`/`DELETE /api/boards/:boardId/lists/:listId/cards/:cardId/members/:memberId`
|
||||
and `.../assignees/:assigneeId`), using `$addToSet`/`$pull` so callers don't
|
||||
read-modify-write the whole array. Adding validates board membership
|
||||
(`canAssignCardMember`) and rejects non-members with 400; removing a stale id is
|
||||
allowed. Wrapped in `api.py` as `addcardmember`/`removecardmember`/
|
||||
`addcardassignee`/`removecardassignee`.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Issue [#5897](https://github.com/wekan/wekan/issues/5897): Create Linked Card.**
|
||||
Linked cards reference another card; the data model uses `linkedId` /
|
||||
`type: 'cardType-linkedCard'`. Implemented by reusing the existing
|
||||
`POST /api/boards/:boardId/lists/:listId/cards` route: when `linkedId` is present
|
||||
in the body the new card is created via `Card.link` (type cardType-linkedCard)
|
||||
instead of holding its own content. Linking across boards is allowed; the caller
|
||||
must have write access to the destination board and read access to the linked
|
||||
card's board (`checkBoardAccess`). Wrapped in `api.py` as `linkcard`.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Fix Issue [#5846](https://github.com/wekan/wekan/issues/5846): Add/Remove card
|
||||
dates (Received/Start/Due/End).** Setting dates already worked via `PUT` card with
|
||||
`receivedAt`, `startAt`, `dueAt`, `endAt`, but the handler only wrote a date when
|
||||
the value was truthy (`if (req.body.receivedAt)`), so an empty string could NOT
|
||||
clear a date — removing a date via the API did not work. Fixed in
|
||||
`server/models/cards.js`: whenever a date field is present in the request body, an
|
||||
empty string / `null` / `"null"` now `$unset`s the date and any other value sets
|
||||
it. `api.py` wraps this as `setcarddate ... DATETYPE [DATEVALUE]` (omit DATEVALUE,
|
||||
or pass an empty string, to clear).
|
||||
Thanks to xet7 and Claude.
|
||||
- **Issue [#5819](https://github.com/wekan/wekan/issues/5819): Bulk add/remove labels
|
||||
with API, and BoardAdmin label management in the right sidebar Labels
|
||||
hamburger/trigram menu.**
|
||||
`PUT` card `labelIds` REPLACES the whole label set (wrapped as `setcardlabels`).
|
||||
Implemented: a multi-card bulk endpoint `POST /api/boards/:boardId/cards/labels`
|
||||
taking `{cardIds, addLabelIds, removeLabelIds}` that MERGES — existing labels are
|
||||
kept, `removeLabelIds` are dropped, `addLabelIds` are added, de-duplicated, across
|
||||
all listed cards in one request. `addLabelIds` are validated against the board's
|
||||
labels (request rejected with the offending ids otherwise); cards not on the board
|
||||
are reported in `notFound`. Wrapped in `api.py` as `bulkcardlabels`.
|
||||
Board label creation via `PUT /api/boards/:boardId/labels` is now gated to
|
||||
BoardAdmin (`allowIsBoardAdmin`); normal members can still apply existing labels
|
||||
to cards (including via the bulk endpoint). Still planned: the right-sidebar
|
||||
Labels hamburger/trigram menu BoardAdmin-only UI. Thanks to xet7 and Claude.
|
||||
- **Fix Issue [#5813](https://github.com/wekan/wekan/issues/5813): Card number is not
|
||||
unique when concurrently creating multiple cards via REST API; many cards got the
|
||||
same card number.** Root cause: `Board.getNextCardNumber()` (`models/boards.js`)
|
||||
read the current max `cardNumber` and returned `max + 1`; two concurrent card
|
||||
creations read the same max and both got the same number — a classic
|
||||
read-then-increment race. Fixed by allocating card numbers from an atomic
|
||||
per-board counter (`Counters.incrementCounterAsync('cardNumber-<boardId>')` in
|
||||
`models/counters.js`), a single atomic `findOneAndUpdate($inc)` that is safe under
|
||||
concurrency. Existing boards (and imported boards with existing cards) are
|
||||
**lazy-seeded** on first use: the counter is initialized to the board's current
|
||||
max `cardNumber` via an idempotent `$setOnInsert` upsert, so no migration is
|
||||
needed and no number an existing card already has is reissued. Card numbers may
|
||||
have gaps (deleting a card does not decrement the counter), which is intended.
|
||||
Numbering stays per-board. The client keeps the old max + 1 read (card numbers are
|
||||
not authoritative there; the server insert recomputes). See also #4743 below.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Copy/Move Swimlane/List/Card to the same or a different board, before/after a
|
||||
position counted (number of swimlanes/lists/cards) from the top-left.**
|
||||
Implemented: target position is a 0-based index counted from the top-left
|
||||
("after N items"); the server converts it to a `sort` value between siblings via
|
||||
`computeSortForIndex` (`server/lib/utils.js`). New endpoints:
|
||||
`POST /api/boards/:boardId/lists/:listId/cards/:cardId/copy`,
|
||||
`POST /api/boards/:boardId/swimlanes/:swimlaneId/copy` and `.../move`,
|
||||
`POST /api/boards/:boardId/lists/:listId/copy` and `.../move`. Copy is a FULL
|
||||
deep copy via the existing model `copy()` methods (cards, checklists,
|
||||
attachments, custom-field values). Destination board write access is required
|
||||
(may differ from source). Wrapped in `api.py` as `copycard`,
|
||||
`copyswimlane`/`moveswimlane`, `copylist`/`movelist`. NOTE: `List.move` merges
|
||||
into an existing same-titled list on the destination board when one exists (a
|
||||
pre-existing model behavior); same-board list move is a pure reposition.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Issue [#4815](https://github.com/wekan/wekan/issues/4815): API to get My Cards and
|
||||
Due Cards (also needs a user-scoped API).**
|
||||
The web UI already has My Cards and Due Cards views. Implemented: a single
|
||||
`GET /api/user/cards` endpoint returning the current user's cards (where they are
|
||||
a member or assignee), with a `?due=true` filter for cards that have a due date
|
||||
and an optional `?from=`/`?to=` (ISO 8601) due-date range. Returns a compact
|
||||
minicard-like field set, sorted by due date. Wrapped in `api.py` as `mycards`.
|
||||
(Cross-user/admin querying, board filter and pagination were not requested for the
|
||||
first version; revisit if needed for users in very many boards.)
|
||||
Thanks to xet7 and Claude.
|
||||
- **Issue [#4811](https://github.com/wekan/wekan/issues/4811): After adding a card via
|
||||
the API, does the card count update properly?**
|
||||
To verify: `GET /api/boards/:boardId/cards_count` and
|
||||
`GET /api/boards/:boardId/lists/:listId/cards_count` after `POST` card creation.
|
||||
Card counts are computed from the cards collection, so they should reflect
|
||||
API-created cards; this needs a regression test confirming counts update
|
||||
immediately after API create/delete (and are not stale due to caching).
|
||||
- **Issue [#3062](https://github.com/wekan/wekan/issues/3062): API for the Card
|
||||
Settings that live under Board Settings.**
|
||||
Board Settings card defaults (which fields/badges show on cards and minicards) are
|
||||
the board's `allows*` toggles. Implemented: `GET`/`PUT
|
||||
/api/boards/:boardId/cardSettings` exposing those board-level `allows*` settings
|
||||
(read requires board access; write requires board write access). `PUT` accepts any
|
||||
subset of the recognized keys and ignores unknown ones. Per-user card presentation
|
||||
settings are intentionally out of scope. Wrapped in `api.py` as `getcardsettings`/
|
||||
`setcardsetting`.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Fix Issue [#4743](https://github.com/wekan/wekan/issues/4743): Using the REST API
|
||||
to manipulate many cards crashes WeKan (100% CPU, server becomes
|
||||
unreachable/unusable, REST calls time out with HTTP 408).**
|
||||
Reported workflow: every night delete all cards in a board and recreate them
|
||||
(create card → edit with custom fields/dates/labels → list all → delete all),
|
||||
done in a tight loop with no delay; WeKan eventually pegs CPU and stops
|
||||
responding. Same root area as #5813 (concurrent card creation), plus the cost of
|
||||
per-card cascade work (activities, server-side reactivity, custom-field defaults,
|
||||
before/after hooks) under bursts.
|
||||
Implemented so far: (1) atomic card numbering (see #5813) removes the hot
|
||||
read-modify-write contention; (2) bulk create/delete endpoints
|
||||
`POST /api/boards/:boardId/lists/:listId/cards/bulk` (body
|
||||
`{authorId, swimlaneId, cards:[...]}`) and
|
||||
`DELETE /api/boards/:boardId/cards/bulk` (body `{cardIds:[...]}`), each capped at
|
||||
500 items per request, so a sync job sends one request instead of hundreds —
|
||||
directly addressing the reporter's nightly delete-all/recreate workflow. Wrapped in
|
||||
`api.py` as `bulkaddcards` / `bulkdeletecards`. Bulk create returns a per-card
|
||||
result/error array; bulk delete returns `{deleted, notFound}` and only touches
|
||||
cards on the given board.
|
||||
Still planned/optional: per-token rate limiting and batched/optional activity
|
||||
logging were considered but not chosen for now; the bulk endpoints plus the atomic
|
||||
card numbering are expected to remove the hot contention. Revisit rate limiting if
|
||||
storms persist.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Fix REST API auth bugs found by the new E2E tests: awaited the membership/admin
|
||||
checks and return explicit status codes (CWE-862).** Several auth helpers
|
||||
(`checkAdminOrCondition`, `checkUserId`) are async, so calling them without
|
||||
`await` let a denied caller's rejection slip past while the handler kept running:
|
||||
on card create that performed the write anyway (auth bypass), and on the board
|
||||
member endpoints the un-awaited rejection surfaced to the client as an HTTP 503.
|
||||
Fixes:
|
||||
- The pre-existing single-card create (`POST .../cards`) and the new bulk-create
|
||||
endpoint now `await` the membership check, so a logged-in non-board-member can
|
||||
no longer create cards.
|
||||
- The board member add/remove/permission endpoints
|
||||
(`POST .../members/:userId/add`, `.../remove`, `POST .../members/:memberId`)
|
||||
now require **board admin** (or site admin), awaited, returning a clean 401/403
|
||||
— instead of the previous un-awaited `checkUserId` (nominally site-admin-only,
|
||||
but effectively bypassed) that 503'd on denial.
|
||||
- Board-label creation (`PUT .../labels`) is gated to board admin / site admin,
|
||||
and it plus `GET /api/user/cards` return explicit 401/403 instead of letting an
|
||||
auth helper throw (which, under Express 4, would otherwise leave the request
|
||||
hanging). These were all found by the new REST API E2E test below. Thanks to
|
||||
xet7 and Claude.
|
||||
- **End-to-end tests for the new REST API behavior and permissions.** A new
|
||||
Playwright spec `tests/playwright/specs/17-rest-api.e2e.js` exercises the new/changed
|
||||
endpoints against a running server (real Bearer-token auth → real HTTP request →
|
||||
MongoDB change verified directly), asserting both correct data AND correct
|
||||
permissions: bulk create/delete (#4743), unique card numbers under bulk create
|
||||
(#5813), bulk label merge + rejection of off-board labels + BoardAdmin-gated label
|
||||
creation (#5819), add board member by named role + invalid-role rejection and
|
||||
card-member add validated against board membership (#5998), add/clear card date
|
||||
(#5846), linked-card creation (#5897), copy-card-to-position deep copy, board card
|
||||
settings GET/PUT (#3062), `GET /api/user/cards` with due filter (#4815), and that an
|
||||
unauthenticated request is denied. These use Playwright's HTTP `request` client (no
|
||||
browser is launched, so they run fast). The pure data/permission helpers they rely
|
||||
on live in `server/lib/utils.js` (`boardMemberRoleToFlags`, `computeSortForIndex`,
|
||||
`mergeLabelIds`, `canAssignCardMember`, `isCardDateClear`).
|
||||
- **`rebuild-wekan.sh` test menu reorganized.** There is now a menu option per test
|
||||
type so each can be run on its own: "Run ALL tests on http://localhost:3000 (start
|
||||
server, progress + summary)" (starts the server and runs import regression + Mocha +
|
||||
Node E2E + Playwright Chromium, streaming progress and printing a per-suite PASS/FAIL
|
||||
summary), plus standalone "Test Mocha unit + security + API-logic tests", "Test import
|
||||
regression", "Test Node E2E regressions", and the existing per-browser Playwright
|
||||
options. Playwright holds the browser-UI specs (01–16) and the API E2E spec (17);
|
||||
the fast unit/security/policy tests stay in the Mocha suite (`meteor test --once
|
||||
--driver-package meteortesting:mocha`).
|
||||
Thanks to xet7 and Claude.
|
||||
- [Fix tests](https://github.com/wekan/wekan/commit/a532ee7f14726dbf1eec956978d08436d7c47550).
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.35 2026-06-06 WeKan ® release
|
||||
|
||||
TLDR:
|
||||
- Admin Panel/Attachments/Move Files works: CollectionFS/Meteor-Files/S3/Azure Blob/Google Cloud Storage.
|
||||
- To have attachments and avatars visible, move them from CollectionFS to any other Storage
|
||||
- To upgrade, mongodump/mongorestore (older with [LD_LIBRARY_PATH](https://github.com/wekan/wekan/blob/main/docs/Backup/Backup.md#backup-wekan-snap-to-directory-dump))
|
||||
to MongoDB 7.x, and copy WRITABLE_PATH files/attachments/avatars if exists,
|
||||
at Snap /var/snap/wekan/common/files/, at Docker /data/files etc.
|
||||
|
||||
This release adds the following [CRITICAL SECURITY FIXES](https://github.com/wekan/wekan/commit/357de728c03113b787065bac2c5832ad77f1a117):
|
||||
|
||||
- [Fix GHSA-qfqv-42qw-vvwh: `cloneBoard` Meteor method has no authorization check — any user can clone (read) any private board by ID (CWE-639, CWE-862)](https://github.com/wekan/wekan/security/advisories/GHSA-qfqv-42qw-vvwh).
|
||||
The `cloneBoard` Meteor method in `models/import.js` copied an entire board —
|
||||
including all cards, comments, attachments, member info and activities —
|
||||
identified solely by a caller-supplied `sourceBoardId`, and performed no
|
||||
authorization check: it never verified that the calling user was a member of
|
||||
(or otherwise permitted to read) the source board. Any authenticated Wekan
|
||||
user who knew a board's ID (board IDs appear in board URLs and remain known to
|
||||
removed members) could call `Meteor.call('cloneBoard', '<targetBoardId>')`
|
||||
over DDP and obtain a permanent, fully-readable copy of that board's contents,
|
||||
even for private boards they had no access to. The method called
|
||||
`exporter.build()` directly, skipping the `canExport()` guard
|
||||
(`models/exporter.js`) that the REST export route correctly enforces. Fixed by
|
||||
requiring `this.userId` and running the same `exporter.canExport(user)` check
|
||||
the export route uses before building/cloning the board, so cloning a board
|
||||
now requires the same read authorization as exporting it.
|
||||
CVSS 6.5 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N).
|
||||
Thanks to dizconnectz for the coordinated disclosure, xet7 and Claude.
|
||||
|
||||
and, while fixing the above, the following similar authorization issues found by code review were fixed (the [CloneBleed](https://wekan.fi/hall-of-fame/clonebleed/) group):
|
||||
|
||||
- [Fix authorization guards that silently never ran in attachment, list, checklist, migration and webhook server methods (CWE-862, CWE-639)](https://github.com/wekan/wekan/blob/main/docs/hall-of-fame/clonebleed/).
|
||||
A review for the same class of bug as `cloneBoard` found several server-side
|
||||
methods whose access checks were present in the source but never actually
|
||||
enforced, plus a few methods missing a check entirely:
|
||||
- `server/models/checklists.js` `moveChecklist`: the membership guard called
|
||||
`allowIsBoardMemberByCard(...)`, but that helper is `async` and was both
|
||||
unimported and un-awaited, so `!allowIsBoardMemberByCard(...)` evaluated
|
||||
`!Promise` (always `false`) and the check never ran — any logged-in user
|
||||
could move a checklist between cards on boards they cannot access. Now
|
||||
imported and awaited, and login is required.
|
||||
- `server/models/lists.js` `updateListSort`: the guard read
|
||||
`typeof allowIsBoardMember === 'function' && !allowIsBoardMember(...)`, but
|
||||
`allowIsBoardMember` was never imported, so the `typeof` was always `false`
|
||||
and the guard never ran — any caller could reorder/re-assign any board's
|
||||
lists by ID. Replaced with a real `hasBoardWriteAccess` check, plus a check
|
||||
that the list actually belongs to the named board, and a login check.
|
||||
- `server/attachmentApi.js` and `server/routes/attachmentApi.js`: the
|
||||
attachment Meteor methods and REST handlers called `board.isBoardMember(...)`,
|
||||
which is not a method on the board model, so the permission check never
|
||||
behaved as intended. Replaced with the real `board.hasMember(...)`, and the
|
||||
upload path now also verifies the target card actually belongs to the named
|
||||
board (so `boardId` cannot be spoofed to a board the caller is a member of).
|
||||
- `server/models/users.js` `applyListWidth`: stored per-board list width with
|
||||
no authorization. Now requires the caller to be a member of the board and
|
||||
requires the list to belong to that board.
|
||||
- `server/models/boards.js` `getBackgroundImageURL`: returned any board's
|
||||
background image URL by ID. Now requires the board to be visible to the
|
||||
caller.
|
||||
- `server/migrations/fixMissingListsMigration.js` and
|
||||
`server/migrations/migrateAttachments.js`: the migration status/execute
|
||||
methods only checked that the caller was logged in. Reading status now
|
||||
requires board visibility, executing a board-rewriting migration now requires
|
||||
board admin (or instance admin), and the attachment migration methods now
|
||||
require board access (global status reads require instance admin).
|
||||
- `server/notifications/outgoing.js`: the webhook delivery method trusted the
|
||||
caller-supplied `integration` object and only checked that *some* integration
|
||||
with that URL existed. It now verifies a matching integration exists on its
|
||||
own board and that the caller is a member of that board, closing a path where
|
||||
any authenticated user could drive webhooks (and, via the two-way response
|
||||
path, overwrite comments) on boards they cannot access.
|
||||
- `server/models/userPositionHistory.js`: the
|
||||
`userPositionHistory.createCheckpoint`, `.getRecent`, `.getCheckpoints` and
|
||||
`.restoreToCheckpoint` methods only checked that the caller was logged in,
|
||||
even though the sibling `positionHistory.track*` methods
|
||||
(`server/methods/positionHistory.js`) already require the board to be visible
|
||||
to the caller — the same PositionHistoryBleed class fixed in v8.20/v8.21.
|
||||
All four now require board visibility (via the shared `isVisibleBy` guard)
|
||||
before reading or restoring position history scoped to a board.
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
and, while auditing that client-side permission checks are also enforced server-side, the following gaps where the server did not re-verify a UI-gated permission were fixed:
|
||||
|
||||
- [Fix client-side permission gates not re-verified server-side (CWE-862, CWE-269)](https://github.com/wekan/wekan/blob/main/docs/hall-of-fame/clonebleed/).
|
||||
The browser UI hides certain actions from read-only members and from
|
||||
non-admins, but those are cosmetic — the server-side `allow` rules and Meteor
|
||||
methods must enforce the same role. An audit found four places that did not:
|
||||
- `server/permissions/customFields.js`: the Custom Field `allow`
|
||||
insert/update/remove rules used `allowIsAnyBoardMember` (mere membership), so
|
||||
read-only/comment-only/worker members could create/modify/delete Custom
|
||||
Fields (board-wide schema) via direct DDP collection writes — the same
|
||||
read-only-write class as GHSA-6733, which had only fixed the REST path. Now
|
||||
uses the new `allowIsAnyBoardMemberWithWriteAccess` write-access helper.
|
||||
- `server/permissions/cardCommentReactions.js`: the reaction `allow` rules used
|
||||
`allowIsBoardMember`, letting read-only members add/remove comment reactions.
|
||||
Reacting is a form of commenting, so it now uses `allowIsBoardMemberCommentOnly`
|
||||
(Normal/Comment-only allowed, Read-only/No-comments denied), matching
|
||||
`CardComments.insert`.
|
||||
- `server/models/boards.js` `archiveBoard`: only required board membership, so
|
||||
any member (including read-only) could archive a board (hiding it for
|
||||
everyone) over DDP, although the UI gates archiving behind board admin. Now
|
||||
requires board admin (or global admin), matching the `Boards.allow`
|
||||
update/remove rules.
|
||||
- `server/models/settings.js` `sendSMTPTestEmail`: only required login, so any
|
||||
authenticated user could trigger the server to send an SMTP test (and have
|
||||
the server's SMTP error messages surfaced to them), although the UI gates it
|
||||
behind global admin. Now requires global admin.
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
and, in the same access-control audit, the attachment write API was tightened:
|
||||
|
||||
- **Security: attachment write operations in the REST/DDP API now require board
|
||||
write access, not just membership (CWE-862, CWE-639).** Both attachment API
|
||||
implementations (`server/routes/attachmentApi.js` HTTP routes and
|
||||
`server/attachmentApi.js` Meteor methods) gated upload / copy / move / delete
|
||||
on `board.hasMember()`, which is true for any active member regardless of role.
|
||||
This let read-only, comment-only, no-comments, worker and assigned-only board
|
||||
members add, copy, move and delete attachments through the API — actions the UI
|
||||
forbids for those roles. They now require board write access (global site
|
||||
admins still allowed); read operations (download / list / info) keep
|
||||
membership-level access. Added security tests in
|
||||
`server/lib/tests/attachmentApi.tests.js`.
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- **[Admin Panel / Attachments](https://github.com/wekan/wekan/commit/a86a087915aac9d204e157b1a698091c079e04e6):
|
||||
"Calculate file counts" on every storage backend,
|
||||
shown right below the "Read" toggle.** Azure Blob Storage and Google Cloud
|
||||
Storage now have a "Calculate file counts" button (new `getAzureStorageStats` /
|
||||
`getGcsStorageStats` methods) that reports how many attachments and avatars are
|
||||
stored on that backend, matching what Filesystem, MongoDB GridFS and S3 already
|
||||
offer. The count is read from the stored file metadata (fast, no cloud API
|
||||
calls). The S3 button was also moved up to sit directly under its "Read"
|
||||
checkbox, so all backends are consistent. Thanks to Claude.
|
||||
- **[Admin Panel / Attachments: the MongoDB GridFS page notes how to make legacy files visible](https://github.com/wekan/wekan/commit/dca9427bfb58532778bc705e4c8c8014ba4b01ae).
|
||||
** A translatable line under the "MongoDB GridFS Storage" title
|
||||
reminds admins: "To have attachments and avatars visible, move them from
|
||||
CollectionFS to any other Storage." Thanks to Claude.
|
||||
- **Admin Panel / Attachments: the Google Cloud Storage page now documents the
|
||||
required bucket permission.** A note under the "GCS Storage" title explains that
|
||||
the service account needs the **Storage Object Admin** role
|
||||
(`roles/storage.objectAdmin`) on the bucket (Cloud Console → Cloud Storage →
|
||||
Buckets → your bucket → Permissions → Grant access → add the service account →
|
||||
role "Storage Object Admin"), and that the bucket must exist in the same
|
||||
project — otherwise "Test connection" fails with `storage.objects.list denied`.
|
||||
Thanks to xet7 and Claude.
|
||||
- **Admin Panel / Attachments: a "Save" button next to the Enabled / Read toggles
|
||||
at the top of each cloud storage.** S3, Azure and Google Cloud Storage now have
|
||||
a Save button directly below the "[ ] Enabled [ ] Read" row, so those toggles
|
||||
can be saved without scrolling to the bottom of the (now quite long) storage
|
||||
page. It saves the same way as the existing bottom Save button — the server
|
||||
merges that provider's config and preserves secrets left blank — and the bottom
|
||||
Save button is kept. (Filesystem and GridFS have only a Read toggle, which
|
||||
already saves the instant it is toggled, so they need no button.) Thanks to Claude.
|
||||
- **Admin Panel / Attachments: bilingual, self-documenting cloud-storage fields
|
||||
(S3, Azure and Google Cloud Storage).** Each cloud-storage setting now guides
|
||||
the admin from top to bottom with both English and translated text, so it is
|
||||
easy to follow along while clicking through the provider's Cloud Console:
|
||||
- the field **label** uses the provider's own console wording in English
|
||||
(e.g. *Access key ID*, *Secret access key*, *Storage account key*,
|
||||
*Service account key (JSON)*), with the translated label shown below it;
|
||||
- a literal **example** of what the value should look like
|
||||
(e.g. `eu-west-1`, an example bucket name / endpoint / connection string),
|
||||
which is intentionally not translated;
|
||||
- a short **description** of the value, in English and then translated;
|
||||
- the **Cloud Console menu path** showing exactly where to find or create the
|
||||
value (e.g. *AWS Console → IAM → Users → your user → Security credentials →
|
||||
Access keys → Create access key*; *Azure Portal → Storage accounts → your
|
||||
account → Access keys → key1*; *Google Cloud Console → IAM & Admin →
|
||||
Service accounts → … → Keys → Add key → Create new key → JSON*), again in
|
||||
English and then translated.
|
||||
The provider field names, examples and menu paths are kept in English so they
|
||||
match what the Cloud Console actually shows, while the descriptions and menu
|
||||
paths are also translatable; section titles, the enable/read/force-path-style
|
||||
checkboxes and the Test/Save buttons remain fully translated. Thanks to Claude.
|
||||
- **Admin Panel / Attachments / Move Attachment: "Repair file locations" button
|
||||
and a persistent "last move" message.** The new **Repair file locations** button
|
||||
scans all attachments and avatars and finds any whose recorded storage
|
||||
(`versions.<v>.storage` / `path` / `meta.gridFsFileId`) no longer matches where
|
||||
the binary actually is — left inconsistent by an interrupted or failed move —
|
||||
and fixes the database to point at the real location: it detects the binary in
|
||||
GridFS (by the `metadata.fileId` stamped on upload, recovering files whose
|
||||
GridFS id reference was lost) or on the filesystem (using the storage
|
||||
strategy's own thorough path resolution, so the repair agrees with what a real
|
||||
download/move would find even when `versions.<v>.path` is stale), then corrects
|
||||
`storage`, `path` and `meta.gridFsFileId` accordingly. Cloud-stored files are
|
||||
left untouched, and files found nowhere are reported as "Not found". The scan is
|
||||
streamed (memory-safe) and shows a per-scope searched / repaired / not-found
|
||||
summary. Separately, after a move finishes the page now keeps showing
|
||||
the last move operation — source → destination (scope) and the date/time as
|
||||
`YYYY-MM-DD HH:MM:SS` — so it is clear what was last done.
|
||||
Thanks to xet7 and Claude.
|
||||
- **SVG image uploads are now sanitized instead of rejected.** Uploaded SVGs
|
||||
(attachments and avatars) are cleaned in place in `onAfterUpload` via the new
|
||||
`models/lib/sanitizeSvg.js`, which removes JavaScript (`<script>`, inline
|
||||
`on*=` event handlers, `javascript:`/`vbscript:` URIs, `<foreignObject>` /
|
||||
`<iframe>` / `<object>` / `<embed>` and similar active content) and XML loops
|
||||
(`<!DOCTYPE>` / `<!ENTITY>` entity-expansion / XXE constructs and
|
||||
`<?xml-stylesheet?>`), so SVG images can be uploaded safely. Thanks to Claude.
|
||||
- **Unified attachment/avatar storage migration: move any → any.** Admin Panel /
|
||||
Attachments / Move Attachment can now move **Attachments, Avatars, or both**,
|
||||
from **any source to any destination** across Filesystem, Meteor-Files GridFS,
|
||||
Cloud (S3/Azure/GCS) and legacy CollectionFS GridFS. The default source is
|
||||
"All Read-enabled storages" (every backend whose Read flag is enabled and whose
|
||||
settings work), so everything can be consolidated into one destination in a
|
||||
single run. All metadata is preserved (board / swimlane / list / card / user /
|
||||
uploaded date / name / type / size), attachment cover references
|
||||
(`cards.coverId`) are remapped when an id changes, and the legacy source is
|
||||
deleted only after the new copy is verified. Thanks to Claude.
|
||||
- **Legacy CollectionFS GridFS is now a first-class storage backend** (read,
|
||||
migrate-from, and export-to) for both attachments and avatars, via the new
|
||||
`models/lib/collectionFsStore.js` (binary keyed by `copies.<coll>.key` in the
|
||||
`cfs_gridfs.<coll>` bucket, metadata in `cfs.<coll>.filerecord`). Thanks to Claude.
|
||||
- **The storage strategy layer is now collection-aware.** `moveToStorage` and the
|
||||
GridFS/Cloud strategies previously hard-coded the `Attachments` collection, so
|
||||
moving avatars to GridFS/cloud would have updated the wrong collection. The
|
||||
factory now carries its collection (`Attachments` or `Avatars`), so **avatars
|
||||
can be stored in Meteor-Files GridFS and cloud**, not only on the filesystem.
|
||||
Attachment behavior is unchanged.
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Update Windows MongoDB](https://github.com/wekan/wekan/commit/beedf2fefa614018ce7ed499465092e814a050d9).
|
||||
Thanks to xet7.
|
||||
- [Update DDP transport and reactivity order for multi-tenant deployment](https://github.com/wekan/wekan/pull/6365).
|
||||
Thanks to italojs.
|
||||
- [Updated to Meteor 3.5-rc.1](https://github.com/wekan/wekan/commit/29b3f52e50ea2ee9bfbfb98d3842ce8a9b8e52e3).
|
||||
Thanks to Meteor developers.
|
||||
- **Documented the attachment / file REST API in the OpenAPI docs** — file
|
||||
upload, download, info, listing a board's files, copy, move and delete (the
|
||||
`/api/attachment/*` endpoints registered via `WebApp.handlers.use()`, which the
|
||||
generator cannot auto-discover) are now documented in `openapi/extra_paths.yml`
|
||||
and injected into the generated docs, including their authentication and
|
||||
permission requirements.
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
and adds the following fixes:
|
||||
|
||||
- **[Admin Panel / Attachments: "Run MongoDB compact" now actually compacts the database](https://github.com/wekan/wekan/commit/dca9427bfb58532778bc705e4c8c8014ba4b01ae).**
|
||||
MongoDB refuses `compact` on an active replica-set primary unless
|
||||
`force: true` is given, so every collection failed with "will not run compact
|
||||
on an active replica set primary … use force:true to force" — and because the
|
||||
usual Meteor setup is a single-node replica set (only a primary, no
|
||||
secondaries), nothing was compacted at all and no disk space was reclaimed. The
|
||||
primary is now compacted with `force: true` (secondaries are still compacted
|
||||
without it, so the primary stays available while they run), so the GridFS
|
||||
collections are actually rewritten and freed space is returned to the
|
||||
filesystem. Thanks to Claude.
|
||||
- **Admin Panel / Attachments: clearer cloud "Test connection" errors, and cloud
|
||||
config inputs are trimmed.** Testing an Azure/GCS/S3 connection used to report a
|
||||
single generic `Incomplete configuration or adapter not installed` even when the
|
||||
real problem was specific — e.g. Azure rejecting the config with `Invalid URL`
|
||||
(a stray space/newline in the account name, or a bad endpoint / connection
|
||||
string). `testCloudConnection` now reports the actual cause: adapter not
|
||||
installed, required fields missing, the adapter's own configuration error (such
|
||||
as `Invalid URL`), or the real `listFiles` error (auth failure, container not
|
||||
found, …). It also pre-validates the config and gives an actionable message
|
||||
before the adapter turns it into a cryptic error — e.g. for Azure it explains
|
||||
that the **Storage account name** must be just the account name (3–24 lowercase
|
||||
letters/numbers, e.g. `wekanstorage`, not a full `https://…` URL and with no
|
||||
spaces), or that the **Connection string** is malformed; and when Azure still
|
||||
returns `Invalid URL` the message appends which field to check. In addition, all
|
||||
cloud-storage text fields are now trimmed when read from the form, so
|
||||
leading/trailing whitespace from copy-paste no longer produces an invalid URL.
|
||||
Thanks to xet7 and Claude.
|
||||
- **[Fixed moving attachments to S3](https://github.com/wekan/wekan/commit/a86a087915aac9d204e157b1a698091c079e04e6)
|
||||
(and other cloud storage) failing and crashing the server.**
|
||||
Uploading to S3 failed two ways, each of which crashed
|
||||
the whole server because the rejection was unhandled and SyncedCron treats
|
||||
those as fatal. The `@tweedegolf/sab-adapter-amazon-s3` adapter uploaded with a
|
||||
`PutObjectCommand` whose `Body` was a live stream: (1) with no `ContentLength`
|
||||
the AWS SDK fell back to a chunked signed upload that needs a decoded content
|
||||
length, which was undefined for a stream of unknown size — `Invalid value
|
||||
"undefined" for header "x-amz-decoded-content-length"`; and (2) if the socket
|
||||
dropped mid-upload (`socket hang up`), the SDK's body-stream promise rejected
|
||||
unhandled. Fixed by uploading the file as a complete in-memory **buffer**
|
||||
(`addFileFromBuffer`) instead of a live stream: a buffer has a known length (no
|
||||
content-length error) and no socket-bound stream to re-reject. The cloud upload
|
||||
promise now never rejects — any failure is captured and exposed via
|
||||
`waitUntilStored()`, which `moveToStorage` checks inside a try/catch so a failed
|
||||
upload leaves the source file intact and is logged cleanly. (Files are moved one
|
||||
at a time, so peak memory is one file.) Thanks to Claude.
|
||||
- **Fixed `TypeError: Cannot read properties of undefined (reading 'on')` when
|
||||
moving attachments between Meteor-Files/GridFS and Filesystem.** If a file's
|
||||
source binary was missing at its recorded location (e.g. a file left
|
||||
half-moved by an earlier interrupted run — storage says "gridfs" but the
|
||||
GridFS id reference is gone, or a filesystem path that no longer exists),
|
||||
`getReadStream()`/`getWriteStream()` returned `undefined` and `moveToStorage`
|
||||
(and `copyFile`) called `.on()` on it, throwing. Both now detect a missing
|
||||
read/write stream, log a clear message (which file, version, from→to storage,
|
||||
and why), **skip that file and leave the source intact** so no data is lost,
|
||||
and continue with the rest. Use the new "Repair file locations" button to fix
|
||||
the underlying inconsistent records. Thanks to Claude.
|
||||
- **Fixed moving attachments/avatars from CollectionFS to Meteor-Files crashing
|
||||
the server, hanging the Admin Panel move at "File 1 / N", and leaving broken
|
||||
avatars.** Three problems in Admin Panel / Attachments / Move Attachment:
|
||||
- **Server crash on the first file** (`uncaughtException: TypeError: Cannot read
|
||||
properties of undefined (reading '_id')` at
|
||||
`AttachmentStoreStrategyGridFs.writeStreamFinished`). The current mongodb
|
||||
driver's GridFS `GridFSBucketWriteStream` `'finish'` event no longer passes
|
||||
the stored file document, so `finishedData._id` threw and killed the process
|
||||
(which is also why the move appeared stuck at "File 1 / 4" — the background
|
||||
job died mid-file and its persisted status was frozen). The GridFS strategy
|
||||
now reads the uploaded file id from the write stream itself (`gridFSFile._id`
|
||||
/ `id`), the `'finish'` handler is wrapped so it can never crash the process,
|
||||
and a startup reconciliation clears a stale "running" move status left by a
|
||||
crashed run so the UI un-sticks and a new move can be started.
|
||||
- **Broken avatar after migrating avatars.** The bulk move only repointed card
|
||||
cover references (`cards.coverId`); a user's `profile.avatarUrl` still pointed
|
||||
at the deleted legacy `/cfs/files/avatars/<oldId>` URL, so the avatar rendered
|
||||
broken. The move now repoints `profile.avatarUrl` to the migrated avatar
|
||||
(`remapReferences` handles avatars), and migrated files stamp
|
||||
`meta.migratedFromId` so references can be repaired to the **exact** new file.
|
||||
The startup repair in `server/models/users.js` (previously a no-op: it
|
||||
string-replaced the URL prefix while keeping the now-deleted id and never
|
||||
saved the document) now points each affected user's avatar at their migrated
|
||||
Meteor-Files avatar — matched precisely by `meta.migratedFromId`, otherwise
|
||||
strictly by `userId` (newest migrated avatar first), so a user can only ever
|
||||
be given their own avatar, never another user's.
|
||||
- **`ObjectID` deprecation warning** from `models/lib/grid/createObjectId.js`
|
||||
(`MongoInternals.NpmModule.ObjectID` → `ObjectId`).
|
||||
Thanks to xet7 and Claude.
|
||||
- [Fix Dropdown list cannot be created with values](https://github.com/wekan/wekan/commit/e01f99eb52812df5e2754c193860002ec2716ecb).
|
||||
- **Fixed Dropdown custom field options not being addable / saving as empty.**
|
||||
Creating a "Dropdown" custom field showed the "List Options" box, but pressing
|
||||
Enter (or typing and saving) never added any option, and on the card the only
|
||||
selectable value was `(none)`. The custom-fields sidebar component was migrated
|
||||
from `BlazeComponent` to a plain `Template`, but the template still iterates the
|
||||
options with `{{#each dropdownItems.get}}` — under BlazeComponent that resolved
|
||||
to the instance's `dropdownItems` ReactiveVar, whereas a plain Template does not
|
||||
expose instance variables to the template, so the list rendered nothing. Because
|
||||
the options were never rendered as inputs, `getDropdownItems()` then overwrote
|
||||
the ReactiveVar with the empty DOM on save and every entered value was dropped.
|
||||
Fixed by re-adding the missing `dropdownItems` helper (returning the ReactiveVar),
|
||||
matching the pattern the other migrated templates already use. Thanks to Claude.
|
||||
- **Fixed `Exception in global helper _` when opening the Create Custom Field
|
||||
popup (and any translation containing a literal `%`).** i18n is configured with
|
||||
a global sprintf post-processor (`postProcess: ["sprintf"]`), so every
|
||||
translation is run through `i18next-sprintf-postprocessor`. The help text
|
||||
`custom-field-stringtemplate-format` (`"Format (use %{value} as placeholder)"`)
|
||||
contains a literal `%{value}` that sprintf cannot parse, so `vsprintf` threw and
|
||||
crashed the global Blaze `_` translation helper — breaking that popup and any
|
||||
string (in any language, including user translation overrides) that contains a
|
||||
stray `%`. `TAPi18n.__` now retries without the sprintf post-processor when it
|
||||
throws, returning the raw string (so `%{value}` is shown literally) instead of
|
||||
crashing.
|
||||
Thanks to rouceto1, xet7 and Claude.
|
||||
- [Fixed new checklists (and checklist items) on a newly added card not being visible until logout/login](https://github.com/wekan/wekan/commit/075e86b00e1f0dc0dea519acd37cece4d0a1fad3).
|
||||
The `board` publication batched checklists,
|
||||
checklist items, comments and attachments into board-level cursors filtered by
|
||||
`cardId: { $in: cardIds }`, where `cardIds` was a one-time snapshot. In
|
||||
`reywood:publish-composite` a child cursor only re-runs when its parent (the
|
||||
board) document changes, so the snapshot never refreshed when a card was added
|
||||
— a checklist on a card created after subscribing matched no published card and
|
||||
only appeared on the next subscribe (logout/login). This was a regression from
|
||||
the "Optimized board loading" change, which had replaced the original reactive
|
||||
per-card child cursors with these batched snapshots. Fixed by **denormalizing a
|
||||
`boardId` field onto `Checklists` and `ChecklistItems`** so they can be
|
||||
published with a single board-level cursor filtered by `boardId` — one cursor
|
||||
per collection (keeping the load optimization) that still reacts to checklists
|
||||
on newly added cards, because a new checklist is created already carrying the
|
||||
board's id. `boardId` is set on insert (server `before.insert` hooks, plus
|
||||
explicitly in the Trello/WeKan board importers, which use `direct.insert` and
|
||||
bypass hooks), re-derived when a checklist/item or its card moves to another
|
||||
card (`before.update` on `cardId`) or the card moves to another board
|
||||
(`Cards.after.update` cascade), and backfilled for existing data by an
|
||||
idempotent startup migration. New `{ boardId: 1 }` indexes were added on both
|
||||
collections. Comments and attachments instead remain reactive as per-card
|
||||
children of the cards cursor. Assigned-only board members still only receive
|
||||
checklists for cards assigned to them (the board-level cursor falls back to the
|
||||
assigned cards' ids for those roles).
|
||||
Thanks to ahlgrimma, xet7 and Claude.
|
||||
- [Fixed SyncedCron crash](https://github.com/wekan/wekan/commit/72767ad9a769fee2548f93bbd7174edd69995e97).
|
||||
**Fixed deleting archived lists (or many cards) crashing the server with
|
||||
`SyncedCron: Fatal error encountered (unhandledRejection): TypeError: Cannot
|
||||
read properties of undefined (reading 'boardId')` at
|
||||
`server/models/checklistItems.js`.** Deleting a list cascades into removing its
|
||||
cards, and each card's checklists, checklist items, comments and attachments
|
||||
(`cardRemover` in `models/cards.js`). `Cards.before.remove` called `cardRemover`
|
||||
**without `await`** (and was not `async`), so the card document was deleted
|
||||
first and the cascade then ran with the parent card already gone; the
|
||||
`ChecklistItems.before.remove` / `Checklists.before.remove` hooks dereferenced
|
||||
the now-undefined card (`card.boardId`) and threw, and because the promise was
|
||||
unhandled, SyncedCron caught the rejection and tore down all running cron jobs.
|
||||
Fixed by making `Cards.before.remove` `async` and awaiting `cardRemover` (so
|
||||
sub-items are removed while the card still exists), and the REST card-delete
|
||||
handler now runs `cardRemover` before removing the card. As defense in depth,
|
||||
every card-activity hook and helper that looked up a card and used its
|
||||
`boardId` / `listId` / `swimlaneId` now skips (with a warning) when the parent
|
||||
card — or, for checklist-completion activities, the parent checklist — is
|
||||
missing, instead of throwing: `before.remove` on checklist items and
|
||||
checklists, `Checklists.after.insert`, the `Cards.before.update` timing
|
||||
activity, and the shared `itemCreation` / `publishCheckActivity` /
|
||||
`publishChekListCompleted` / `publishChekListUncompleted` / `commentCreation`
|
||||
helpers (matching the guards already present in
|
||||
`server/models/cardComments.js`).
|
||||
Thanks to titver968, xet7 and Claude.
|
||||
- **[Fixed upgrade crash](https://github.com/wekan/wekan/commit/39f3c89b0a11c5671b77d3a0b95200ac7256a4f1)
|
||||
`An error occurred when creating an index for collection
|
||||
"users": Topology is closed` / `MongoServerSelectionError: Server selection
|
||||
timed out after 30000 ms`.** When WeKan started before MongoDB was reachable
|
||||
and had an elected replica-set primary (common right after an upgrade, while
|
||||
MongoDB replays its WiredTiger journal, or when the app container starts at the
|
||||
same instant as the database), the first index creation threw and Node exited.
|
||||
Now:
|
||||
- **WeKan waits until MongoDB is ready** (reachable, with an elected primary)
|
||||
before it starts, in every launch path: `start-wekan.sh`, `start-wekan.bat`,
|
||||
the Snap (`snap-src/bin/wekan-control`), and the app itself
|
||||
(`server/00waitForMongo.js` / `server/lib/mongoStartup.js`, which blocks the
|
||||
first `Meteor.startup` so it also protects the plain Docker image). The Docker
|
||||
`docker-compose.yml` and `docker-compose-multitenancy.yml` now give `wekandb`
|
||||
a `healthcheck` (primary elected) and the WeKan/tenant services
|
||||
`depends_on: condition: service_healthy`.
|
||||
- **If MongoDB stays unreachable for too long** (default 120s, configurable via
|
||||
`WEKAN_DB_WAIT_TIMEOUT`), a clear English-only message is printed to the
|
||||
Node.js console / `docker logs` / `snap logs` explaining that a database
|
||||
upgrade with `mongodump` (old MongoDB) and `mongorestore --drop` (new
|
||||
MongoDB) may be needed, and reminding that attachments and avatars live
|
||||
on disk under `WRITABLE_PATH` (`files`, `attachments`, `avatars`; on Snap
|
||||
`/var/snap/wekan/common/files`, on Docker the `wekan-files` volume at
|
||||
`/data`) and must be copied too. WeKan keeps retrying after printing it.
|
||||
- **Index creation is now idempotent and crash-safe.** A new `ensureIndex`
|
||||
helper checks the existing indexes and only creates the ones that are
|
||||
missing, and never throws — a single index problem is logged in English
|
||||
instead of taking the whole server down. All startup index creation across
|
||||
the model files was switched to it.
|
||||
Thanks to xet7 and Claude.
|
||||
- [Fixed OpenAPI REST API documentation generation](https://github.com/wekan/wekan/commit/c71a97cba20247ce227a288fa74ef23cb3c4c83e),
|
||||
which had been broken
|
||||
since after WeKan v7.93 and only generated docs for the `login`/`register`
|
||||
endpoints (2 operations) instead of the full API. The Meteor 3 migration moved
|
||||
the REST routes from `models/*.js` (`JsonRoutes.add(...)`) into
|
||||
`server/models/*.js` (`WebApp.handlers.get/post/put/delete(...)`) and
|
||||
introduced optional chaining (`?.`) that the `esprima` Python parser cannot
|
||||
read, so `openapi/generate_openapi.py` silently skipped every route file.
|
||||
The generator now understands both routing styles, scans both `models/` and
|
||||
`server/models/`, downlevels modern JS syntax so files parse, handles the
|
||||
`type: Array` SimpleSchema idiom, and `releases/rebuild-docs.sh` works directly
|
||||
with Python 3.12.x (PEP 668). The generated `public/api/wekan.yml` /
|
||||
`wekan.html` now cover the full API again (89 operations / 61 paths).
|
||||
Thanks to xet7 and Claude.
|
||||
- **The attachment copy API now honours the admin "Admin Panel / Attachments"
|
||||
API transfer limits.** Copying an attachment creates a new attachment but
|
||||
skipped the `apiUploadBlocked` / `apiUploadMaxBytes` checks that upload
|
||||
enforces; copy now respects them in both API implementations. Thanks to Claude.
|
||||
- [Fixed Admin Panel / Attachments / Move Attachment doing nothing / crashing](https://github.com/wekan/wekan/commit/d30803276d6d06db44f65ee1fb583cc11aac8b7b).
|
||||
Several issues in the bulk move:
|
||||
- The GridFS source matcher required `meta.gridFsFileId` to be *absent*, but
|
||||
Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero
|
||||
files and "nothing happened". The matcher now recognizes real GridFS files
|
||||
(`versions.*.storage === 'gridfs'` or a `meta.gridFsFileId` reference), and a
|
||||
"nothing to move" message is shown when a source is empty instead of silently
|
||||
doing nothing.
|
||||
- Moving files crashed the whole server with
|
||||
`FilesCollection#findOne() not available in server` — `ReactiveCache.getAttachment`
|
||||
used a synchronous `findOne()`; it now uses `findOneAsync()`. The background
|
||||
job is also hardened so a single failing file is skipped instead of crashing
|
||||
the server via an unhandled rejection.
|
||||
- The attachment **copy** API now honours the admin API upload limits (it
|
||||
previously skipped them).
|
||||
- **Fixed the "MongoDB Meteor-Files" file-count statistic** in Admin Panel /
|
||||
Attachments, which counted *every* attachment metadata document (so files on
|
||||
the Filesystem were wrongly reported as being in GridFS). It now counts only
|
||||
attachments actually stored in GridFS, consistent with the move tool. Also
|
||||
renamed the mislabeled "Mongo-Files" column to "Meteor-Files". Thanks to Claude.
|
||||
- **Read legacy CollectionFS attachments and avatars in place** (without
|
||||
migrating). The backward-compatibility layer
|
||||
(`models/lib/attachmentBackwardCompatibility.js`) was broken — it looked up the
|
||||
GridFS binary by the filerecord `_id` and by filename instead of by
|
||||
`ObjectId(copies.<coll>.key)`, so legacy files were never found. It is fixed and
|
||||
generalized for attachments and avatars. Legacy attachments now appear in the
|
||||
card attachment gallery (new `legacyBoardAttachments` publication) and stream
|
||||
from the `cfs_gridfs.attachments` bucket, and legacy avatars
|
||||
(`/cfs/files/avatars/<id>` URLs) are served from the `cfs_gridfs.avatars`
|
||||
bucket instead of redirecting to a 404.
|
||||
Thanks to xet7 and Claude.
|
||||
- [Ask to install npm dependencies before running tests](https://github.com/wekan/wekan/commit/b77b62bfa2e7a63fdd2d2d071693d5aa485a34c5).
|
||||
Thanks to xet7 and Claude.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.34 2026-05-31 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix to not list all boards etc at Admin Panel / Attachments / Move Attachment](https://github.com/wekan/wekan/commit/fc10ce425910ce35fb6ea03a81b20ff870ac347e).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v9.33 2026-05-31 WeKan ® release
|
||||
|
||||
This release adds [the following fixes](https://github.com/wekan/wekan/commit/6f021aa387d8c388821119b7f564565892ccc5ca), In Progress:
|
||||
|
||||
+6
-1
@@ -3,9 +3,14 @@
|
||||
For all code at WeKan GitHub Organization https://github.com/wekan
|
||||
|
||||
- All code in pull requests need to have permission already to add it to WeKan with MIT license, and will become MIT license.
|
||||
- All code xet7 add is MIT license.
|
||||
- For any dependencies, permissive licenses like https://copyfree.org are preferred
|
||||
- For anything currently that is non-permissive (like GPL, AGPL, SSPL), those will be replaced with permissive-licensed alternatives
|
||||
- Any intelligence allowed: Human, AI or any other. No discrimination, please have world peace. Mentioning participants optional.
|
||||
- All code is checked and validated by xet7 and many different security AI. If code is real working fix or feature, it usually will be merged.
|
||||
- All known vulnerabilities have been fixed at newest WeKan, please upgrade https://wekan.fi/upgrade/
|
||||
- There is no known backdoors. If WeKan is installed by someone else, first registered user is Admin and has Admin Panel access.
|
||||
- Maintainer of WeKan has no access to someone elses installed WeKan.
|
||||
- For someone elses installed WeKan, see [Forgot Password](https://github.com/wekan/wekan/blob/main/docs/Login/Forgot-Password.md)
|
||||
|
||||
# Reporting about violations or something else
|
||||
|
||||
|
||||
+10
-4
@@ -34,6 +34,16 @@ and click send PR.
|
||||
|
||||
## Contributing code
|
||||
|
||||
Use rebuild-wekan.sh to run all tests to verify you don't break any features, before sending PR.
|
||||
|
||||
Only send one feature or fix at one PR. Do not add many unrelated changes.
|
||||
|
||||
Only send those changed code lines, that are related to your one feature or fix.
|
||||
|
||||
Do not format unrelated code. Do not use code formatting or linting like eslist or prettier.
|
||||
|
||||
Send your code ASAP, because there are daily releases of WeKan with major features and fixes added.
|
||||
|
||||
[Building WeKan and sending PR](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md).
|
||||
|
||||
WeKan code contributors Hall of Fame is at ChangeLog, where
|
||||
@@ -49,10 +59,6 @@ Also see other docs:
|
||||
|
||||
https://github.com/wekan/wekan/tree/main/docs/DeveloperDocs
|
||||
|
||||
Do not use code formatting or linting like eslist or prettier.
|
||||
|
||||
Only send minimal changed code lines, that are related to feature or fix.
|
||||
|
||||
WeKan code has MIT license.
|
||||
|
||||
About 300 persons have contributed to WeKan, stats at:
|
||||
|
||||
+8
-3
@@ -7,7 +7,7 @@ LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
|
||||
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=9.33
|
||||
ARG VERSION=9.42
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
@@ -160,9 +160,14 @@ ENV \
|
||||
|
||||
RUN <<EOR
|
||||
set -o xtrace
|
||||
# Fail hard on any error so a missing release zip / failed download can never
|
||||
# produce a "successful" image with an empty /build (Cannot find /build/main.js).
|
||||
set -eo pipefail
|
||||
|
||||
# Create Wekan user
|
||||
useradd --user-group --system --home-dir /home/wekan wekan
|
||||
# Create Wekan user. --create-home is required because --system users do not
|
||||
# get a home directory by default; without it /home/wekan never exists and the
|
||||
# later `chown ... /home/wekan/` aborts the build (now that set -e is active).
|
||||
useradd --user-group --system --create-home --home-dir /home/wekan wekan
|
||||
|
||||
# OS Updates
|
||||
apt-get update --assume-yes
|
||||
|
||||
+26
-28
@@ -7,9 +7,11 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
|
||||
### 1. Security Researcher:
|
||||
|
||||
- **Report**: Please email details regarding security vulnerabilities to **security@wekan.fi**.
|
||||
- **Report**: Click "New draft security advisory" at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
|
||||
More info at [GitHub Docs about reporting privately](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/report-privately).
|
||||
- **Proof of Concept (PoC)**: If possible, include a fix or a reproduction script/code.
|
||||
- **Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
|
||||
- **If GitHub does not work**: Please send report to **security@wekan.fi**
|
||||
- **Optional Email Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
|
||||
|
||||
### 2. WeKan Security Team:
|
||||
|
||||
@@ -17,17 +19,14 @@ because WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
Fixes are announced at the top of the [ChangeLog](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
|
||||
- **Recognition**: We will acknowledge your contribution by adding you to our
|
||||
[Hall of Fame](https://wekan.fi/hall-of-fame/).
|
||||
- **CVE Policy**: WeKan Security does not facilitate **CVE Coordination** due to
|
||||
our rapid release cycle (new versions are released almost daily).
|
||||
- **CVE Policy**: WeKan Security requests CVE at GitHub when releasing Security Advisory.
|
||||
|
||||
### 3. Post-Release and Public Disclosure:
|
||||
|
||||
- **Independent CVE Assignment**: After the fix is public, you are welcome to coordinate
|
||||
**CVE assignment** independently. Please send the assigned CVE IDs to **security@wekan.fi**,
|
||||
and we will update your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/).
|
||||
- Publicity: If you publish blog posts, give talks, or write articles about the
|
||||
reported vulnerabilities, please send the links to us. We will link them
|
||||
to your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/) for additional context.
|
||||
- Advisories reported at GitHub are listed at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
|
||||
|
||||
## How Should Reports Be Formatted?
|
||||
|
||||
@@ -51,34 +50,34 @@ added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
|
||||
|
||||
## Which domains are in scope?
|
||||
|
||||
No public domains, because all those are donated to Wekan Open Source project,
|
||||
No public domains, because all those are donated to WeKan Open Source project,
|
||||
and we don't have any permissions to do security scans on those donated servers.
|
||||
|
||||
Please don't perform research that could impact other users. Second, please keep
|
||||
the reports short and succinct. If we fail to understand the logic of your bug, we will tell you.
|
||||
|
||||
You can [Install Wekan](https://wekan.fi/install/) on your own computer
|
||||
You can [Install WeKan](https://wekan.fi/install/) on your own computer
|
||||
and scan it's vulnerabilities there.
|
||||
|
||||
## About Wekan Versions
|
||||
## About WeKan Versions
|
||||
|
||||
There are 2 versions of Wekan: Standalone Wekan, and Sandstorm Wekan.
|
||||
There are 2 versions of WeKan: Standalone WeKan, and Sandstorm WeKan.
|
||||
|
||||
## Meteor Security
|
||||
|
||||
https://guide.meteor.com/security
|
||||
|
||||
### Standalone Wekan Security
|
||||
### Standalone WeKan Security
|
||||
|
||||
Standalone Wekan includes all non-Sandstorm platforms. Some Standalone Wekan platforms
|
||||
Standalone WeKan includes all non-Sandstorm platforms. Some Standalone WeKan platforms
|
||||
like Snap and Docker have their own specific sandboxing etc features.
|
||||
|
||||
Standalone Wekan by default does not load any files from Internet, like fonts, CSS, etc.
|
||||
This also means all Standalone Wekan functionality works in offline local networks.
|
||||
Standalone WeKan by default does not load any files from Internet, like fonts, CSS, etc.
|
||||
This also means all Standalone WeKan functionality works in offline local networks.
|
||||
WeKan is used in most countries of the world https://snapcraft.io/wekan
|
||||
and by companies that have 30k users.
|
||||
|
||||
- Wekan private board attachments are not accessible without logging in.
|
||||
- WeKan private board attachments are not accessible without logging in.
|
||||
- There is feature to set board public, so that board is visible without logging
|
||||
in readonly mode, with realtime updates.
|
||||
- Admin Panel has feature to disable all public boards, so all boards are private.
|
||||
@@ -96,12 +95,12 @@ and by companies that have 30k users.
|
||||
- WeKan uses DOMPurify npm package to filter for XSS at fields like cards, as you can see from
|
||||
[package.json](https://github.com/wekan/wekan/blob/main/package.json). Other used versions can be seen from
|
||||
[Meteor versions file](https://github.com/wekan/wekan/blob/main/.meteor/versions).
|
||||
- Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
|
||||
- Forms can include markdown links, html, image tags etc like you see at https://wekan.fi .
|
||||
- It's possible to add attachments to cards, and markdown/html links to files.
|
||||
- DOMPurify cleans up viewed code, so JavaScript in input fields does not execute
|
||||
- https://wekan.github.io/hall-of-fame/fieldbleed/
|
||||
- https://wekan.fi/hall-of-fame/fieldbleed/
|
||||
- Reaction in comment is now checked, that it does not have extra added code
|
||||
- https://wekan.github.io/hall-of-fame/reactionbleed/
|
||||
- https://wekan.fi/hall-of-fame/reactionbleed/
|
||||
- https://github.com/wekan/wekan/blob/main/packages/markdown/src/template-integration.js#L76
|
||||
|
||||
## QA About PubSub
|
||||
@@ -131,7 +130,7 @@ A:
|
||||
PubSub/websocket data (for example with Burp Suite Community Edition), etc.
|
||||
- If you have a REST API, also check that only those who have a login token and permission can view or edit text.
|
||||
- You should not include any data user is not allowed to see. Not to webpage text, not to websockets/PubSub, etc.
|
||||
- Minimongo should not have password hashes PubSub https://wekan.github.io/hall-of-fame/userbleed/
|
||||
- Minimongo should not have password hashes PubSub https://wekan.fi/hall-of-fame/userbleed/
|
||||
- PubSub uses WebSockets, so you need those to be enabled on the webserver like Caddy/Nginx/Apache etc, examples of settings
|
||||
at right menu of https://github.com/wekan/wekan/wiki
|
||||
- Clientside https://github.com/wekan/wekan/tree/main/client/components subscribes to
|
||||
@@ -155,7 +154,7 @@ A:
|
||||
|
||||
- With PubSub, there is checking, that someone modifying Websockets content, like permission isAdmin, can not change to Admin.
|
||||
- https://github.com/wekan/wekan/commit/cbad4cf5943d47b916f64b4582f8ca76a9dfd743
|
||||
- https://wekan.github.io/hall-of-fame/adminbleed/
|
||||
- https://wekan.fi/hall-of-fame/adminbleed/
|
||||
|
||||
## Permissions and Roles
|
||||
|
||||
@@ -191,12 +190,12 @@ Meteor.startup(() => {
|
||||
|
||||
- Someone reported that it was a problem that the content of HTML comments in edit mode was not visible in view mode, so this change makes HTML comments visible.
|
||||
- https://github.com/wekan/wekan/commit/167863d95711249e69bb3511175d73b34acbbdb3
|
||||
- https://wekan.github.io/hall-of-fame/invisiblebleed/
|
||||
- https://wekan.fi/hall-of-fame/invisiblebleed/
|
||||
|
||||
## Attachments: XSS in filename is sanitized
|
||||
|
||||
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||
- https://wekan.github.io/hall-of-fame/filebleed/
|
||||
- https://wekan.fi/hall-of-fame/filebleed/
|
||||
|
||||
### Attachments: Forced download to prevent stored XSS
|
||||
|
||||
@@ -254,7 +253,7 @@ Meteor.startup(() => {
|
||||
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
|
||||
- https://github.com/wekan/wekan/tree/main/packages/wekan-accounts-lockout
|
||||
|
||||
### Sandstorm Wekan Security
|
||||
### Sandstorm WeKan Security
|
||||
|
||||
On the Sandstorm platform, Standalone WeKan features like the Admin Panel are turned off using environment variables, because Sandstorm platform provides SSO for all apps running on Sandstorm.
|
||||
|
||||
@@ -267,7 +266,7 @@ Also read [Sandstorm Security Practices](https://docs.sandstorm.io/en/latest/usi
|
||||
[Sandstorm Security non-events](https://docs.sandstorm.io/en/latest/using/security-non-events/).
|
||||
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
|
||||
|
||||
## What Wekan bugs are eligible?
|
||||
## What WeKan bugs are eligible?
|
||||
|
||||
Any typical web security bugs. If any of the previously mentioned is somehow problematic and
|
||||
a security issue, we'd like to know about it, and also how to fix it:
|
||||
@@ -279,7 +278,7 @@ a security issue, we'd like to know about it, and also how to fix it:
|
||||
- Authentication bypass
|
||||
- Server-side code execution
|
||||
|
||||
## What Wekan bugs are NOT eligible?
|
||||
## What WeKan bugs are NOT eligible?
|
||||
|
||||
Typical already-known or 'no impact' bugs such as:
|
||||
|
||||
@@ -287,5 +286,4 @@ Typical already-known or 'no impact' bugs such as:
|
||||
- Denial of service
|
||||
- SSL BEAST/CRIME/etc. WeKan does not have SSL built-in; it uses Caddy/Nginx/Apache on the front end.
|
||||
|
||||
Wekan is Open Source with MIT license, and free to use also for commercial use.
|
||||
We welcome all fixes to improve security by email to security@wekan.fi
|
||||
WeKan is Open Source with MIT license, and free to use also for commercial use.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v9.33.0"
|
||||
appVersion: "v9.42.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -71,7 +71,52 @@ If *nix: chmod +x api.py => ./api.py users
|
||||
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
|
||||
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
|
||||
|
||||
|
||||
Board Member API (Issue #5998):
|
||||
python3 api.py addboardmember BOARDID USERID ROLE # Add/activate board member. ROLE: admin, normal, comment, readonly, worker, normalassignedonly, commentassignedonly, readassignedonly, nocomments
|
||||
python3 api.py removeboardmember BOARDID USERID # Remove (deactivate) board member
|
||||
python3 api.py setboardmemberrole BOARDID USERID ROLE # Change permission/role of existing board member
|
||||
|
||||
Card Member / Assignee API (board member -> card member/assignee):
|
||||
python3 api.py setcardmembers BOARDID LISTID CARDID MEMBERIDS # Set card members. MEMBERIDS = comma-separated userIds, or '' to clear
|
||||
python3 api.py setcardassignees BOARDID LISTID CARDID ASSIGNEEIDS # Set card assignees. ASSIGNEEIDS = comma-separated userIds, or '' to clear
|
||||
|
||||
Card Dates API (Issue #5846):
|
||||
python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE [DATEVALUE] # DATETYPE: received, start, due, end. DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z. Omit DATEVALUE (or pass '') to clear the date.
|
||||
|
||||
Card Labels API (Issue #5819):
|
||||
python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS # Set (replace) card labels. LABELIDS = comma-separated labelIds, or '' to clear
|
||||
|
||||
Card Copy/Move API:
|
||||
python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID # Move card to another board/swimlane/list
|
||||
|
||||
Bulk Card API (Issue #4743, #5819):
|
||||
python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE # Create many cards in one request. CARDSJSONFILE = JSON file with a list of titles, or a list of card objects {title,description,members,assignees,...}
|
||||
python3 api.py bulkdeletecards BOARDID CARDIDS # Delete many cards in one request. CARDIDS = comma-separated cardIds
|
||||
python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS # Merge-add/remove labels across many cards. CARDIDS/ADDLABELIDS/REMOVELABELIDS = comma-separated, or '' for none
|
||||
|
||||
Linked Card API (Issue #5897):
|
||||
python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID # Create a linked card in BOARDID/LISTID that references LINKEDCARDID (may be on another board)
|
||||
|
||||
Card Member/Assignee Merge API (Issue #5998):
|
||||
python3 api.py addcardmember BOARDID LISTID CARDID MEMBERID # Add one board member to a card (validated)
|
||||
python3 api.py removecardmember BOARDID LISTID CARDID MEMBERID # Remove one member from a card
|
||||
python3 api.py addcardassignee BOARDID LISTID CARDID ASSIGNEEID # Add one assignee to a card (validated)
|
||||
python3 api.py removecardassignee BOARDID LISTID CARDID ASSIGNEEID # Remove one assignee from a card
|
||||
|
||||
Copy/Move Swimlane/List/Card API (position = 0-based index from top-left):
|
||||
python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION] # Copy a card (deep copy)
|
||||
python3 api.py copyswimlane BOARDID SWIMLANEID [TOBOARDID] [POSITION] # Copy a swimlane (deep copy)
|
||||
python3 api.py moveswimlane BOARDID SWIMLANEID [TOBOARDID] [POSITION] # Move a swimlane
|
||||
python3 api.py copylist BOARDID LISTID TOSWIMLANEID [TOBOARDID] [POSITION] # Copy a list (deep copy)
|
||||
python3 api.py movelist BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION] # Move a list
|
||||
|
||||
My Cards / Due Cards API (Issue #4815):
|
||||
python3 api.py mycards [due] [FROM] [TO] # Current user's cards. 'due' = only cards with a due date; FROM/TO = ISO 8601 due-date range
|
||||
|
||||
Card Settings API (Issue #3062):
|
||||
python3 api.py getcardsettings BOARDID # Get board-level card settings (allows* toggles)
|
||||
python3 api.py setcardsetting BOARDID KEY VALUE # Set one board card setting, e.g. allowsDueDate true
|
||||
|
||||
Admin API:
|
||||
python3 api.py newuser USERNAME EMAIL PASSWORD
|
||||
"""
|
||||
@@ -1035,3 +1080,411 @@ if arguments >= 1:
|
||||
# ------- DELETE ATTACHMENT END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
|
||||
|
||||
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS START -----------
|
||||
# These commands wrap REST endpoints that already exist in Wekan.
|
||||
# See CHANGELOG.md "Upcoming" section for related issues and design notes.
|
||||
|
||||
# Map a human readable ROLE to the board member permission boolean flags
|
||||
# used by /api/boards/:boardId/members/:userId/add and .../members/:memberId
|
||||
def board_role_flags(role):
|
||||
flags = {
|
||||
'isAdmin': 'false',
|
||||
'isNoComments': 'false',
|
||||
'isCommentOnly': 'false',
|
||||
'isWorker': 'false',
|
||||
'isNormalAssignedOnly': 'false',
|
||||
'isCommentAssignedOnly': 'false',
|
||||
'isReadOnly': 'false',
|
||||
'isReadAssignedOnly': 'false',
|
||||
}
|
||||
role = (role or 'normal').lower()
|
||||
rolemap = {
|
||||
'admin': 'isAdmin',
|
||||
'nocomments': 'isNoComments',
|
||||
'comment': 'isCommentOnly',
|
||||
'commentonly': 'isCommentOnly',
|
||||
'worker': 'isWorker',
|
||||
'normalassignedonly': 'isNormalAssignedOnly',
|
||||
'commentassignedonly': 'isCommentAssignedOnly',
|
||||
'readonly': 'isReadOnly',
|
||||
'readassignedonly': 'isReadAssignedOnly',
|
||||
}
|
||||
if role in ('normal', 'member'):
|
||||
return flags # all false == Normal board member
|
||||
if role in rolemap:
|
||||
flags[rolemap[role]] = 'true'
|
||||
return flags
|
||||
print("Invalid ROLE. Valid: admin, normal, comment, readonly, worker, "
|
||||
"normalassignedonly, commentassignedonly, readassignedonly, nocomments")
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == 'addboardmember':
|
||||
# ------- ADD BOARD MEMBER START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py addboardmember BOARDID USERID ROLE")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
userid = sys.argv[3]
|
||||
role = sys.argv[4] if arguments > 3 else 'normal'
|
||||
add_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'add'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = {'action': 'add'}
|
||||
post_data.update(board_role_flags(role))
|
||||
body = requests.post(add_member_url, data=post_data, headers=headers)
|
||||
print("=== ADD BOARD MEMBER ===\n")
|
||||
print(body.text)
|
||||
# ------- ADD BOARD MEMBER END -----------
|
||||
|
||||
if sys.argv[1] == 'removeboardmember':
|
||||
# ------- REMOVE BOARD MEMBER START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py removeboardmember BOARDID USERID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
userid = sys.argv[3]
|
||||
remove_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'remove'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = {'action': 'remove'}
|
||||
body = requests.post(remove_member_url, data=post_data, headers=headers)
|
||||
print("=== REMOVE BOARD MEMBER ===\n")
|
||||
print(body.text)
|
||||
# ------- REMOVE BOARD MEMBER END -----------
|
||||
|
||||
if sys.argv[1] == 'setboardmemberrole':
|
||||
# ------- SET BOARD MEMBER ROLE START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py setboardmemberrole BOARDID USERID ROLE")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
userid = sys.argv[3]
|
||||
role = sys.argv[4]
|
||||
set_role_url = wekanurl + apiboards + boardid + s + 'members' + s + userid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = board_role_flags(role)
|
||||
body = requests.post(set_role_url, data=post_data, headers=headers)
|
||||
print("=== SET BOARD MEMBER ROLE ===\n")
|
||||
print(body.text)
|
||||
# ------- SET BOARD MEMBER ROLE END -----------
|
||||
|
||||
if sys.argv[1] in ('setcardmembers', 'setcardassignees'):
|
||||
# ------- SET CARD MEMBERS / ASSIGNEES START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py {} BOARDID LISTID CARDID IDS".format(sys.argv[1]))
|
||||
print("IDS = comma-separated userIds, or '' to clear")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
ids_raw = sys.argv[5] if arguments > 4 else ''
|
||||
ids = [x for x in ids_raw.split(',') if x] if ids_raw else []
|
||||
field = 'members' if sys.argv[1] == 'setcardmembers' else 'assignees'
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
# Empty array clears the field; send as JSON so the array type is preserved.
|
||||
put_data = {field: ids if ids else []}
|
||||
body = requests.put(edcard, json=put_data, headers=headers)
|
||||
print("=== SET CARD {} ===\n".format(field.upper()))
|
||||
print(body.text)
|
||||
# ------- SET CARD MEMBERS / ASSIGNEES END -----------
|
||||
|
||||
if sys.argv[1] == 'setcarddate':
|
||||
# ------- SET CARD DATE START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE DATEVALUE")
|
||||
print("DATETYPE: received, start, due, end")
|
||||
print("DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
datetype = sys.argv[5].lower()
|
||||
datevalue = sys.argv[6] if arguments > 5 else ''
|
||||
datemap = {
|
||||
'received': 'receivedAt',
|
||||
'start': 'startAt',
|
||||
'due': 'dueAt',
|
||||
'end': 'endAt',
|
||||
}
|
||||
if datetype not in datemap:
|
||||
print("Invalid DATETYPE. Valid: received, start, due, end")
|
||||
sys.exit(1)
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
put_data = {datemap[datetype]: datevalue}
|
||||
body = requests.put(edcard, data=put_data, headers=headers)
|
||||
print("=== SET CARD DATE {} ===\n".format(datetype.upper()))
|
||||
print(body.text)
|
||||
# ------- SET CARD DATE END -----------
|
||||
|
||||
if sys.argv[1] == 'setcardlabels':
|
||||
# ------- SET CARD LABELS START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS")
|
||||
print("LABELIDS = comma-separated labelIds, or '' to clear")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
labelids_raw = sys.argv[5] if arguments > 4 else ''
|
||||
labelids = [x for x in labelids_raw.split(',') if x] if labelids_raw else []
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
# NOTE: this REPLACES the card's labels. Bulk add/remove (merge) is not yet
|
||||
# implemented server-side, see Issue #5819 in CHANGELOG.md "Upcoming".
|
||||
put_data = {'labelIds': labelids if labelids else []}
|
||||
body = requests.put(edcard, json=put_data, headers=headers)
|
||||
print("=== SET CARD LABELS ===\n")
|
||||
print(body.text)
|
||||
# ------- SET CARD LABELS END -----------
|
||||
|
||||
if sys.argv[1] == 'movecard':
|
||||
# ------- MOVE CARD START -----------
|
||||
if arguments < 7:
|
||||
print("Usage: python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
newboardid = sys.argv[5]
|
||||
newswimlaneid = sys.argv[6]
|
||||
newlistid = sys.argv[7]
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
put_data = {
|
||||
'newBoardId': newboardid,
|
||||
'newSwimlaneId': newswimlaneid,
|
||||
'newListId': newlistid,
|
||||
}
|
||||
body = requests.put(edcard, data=put_data, headers=headers)
|
||||
print("=== MOVE CARD ===\n")
|
||||
print(body.text)
|
||||
# ------- MOVE CARD END -----------
|
||||
|
||||
if sys.argv[1] == 'bulkaddcards':
|
||||
# ------- BULK ADD CARDS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE")
|
||||
print("CARDSJSONFILE = JSON file with a list of titles, e.g. [\"Card A\", \"Card B\"],")
|
||||
print(" or a list of card objects [{\"title\": \"Card A\", \"description\": \"...\"}, ...]")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
authorid = sys.argv[4]
|
||||
swimlaneid = sys.argv[5]
|
||||
cardsjsonfile = sys.argv[6] if arguments > 5 else None
|
||||
if not cardsjsonfile:
|
||||
print("CARDSJSONFILE is required")
|
||||
exit(1)
|
||||
try:
|
||||
with open(cardsjsonfile, 'r') as f:
|
||||
raw_cards = json.load(f)
|
||||
except Exception as e:
|
||||
print("Error reading CARDSJSONFILE: {}".format(e))
|
||||
exit(1)
|
||||
if not isinstance(raw_cards, list) or len(raw_cards) == 0:
|
||||
print("CARDSJSONFILE must contain a non-empty JSON list")
|
||||
exit(1)
|
||||
cards = []
|
||||
for item in raw_cards:
|
||||
if isinstance(item, str):
|
||||
cards.append({'title': item})
|
||||
elif isinstance(item, dict):
|
||||
cards.append(item)
|
||||
else:
|
||||
print("Each card must be a string (title) or an object")
|
||||
exit(1)
|
||||
bulk_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + 'bulk'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'cards': cards}
|
||||
body = requests.post(bulk_url, json=post_data, headers=headers)
|
||||
print("=== BULK ADD CARDS ===\n")
|
||||
print(body.text)
|
||||
# ------- BULK ADD CARDS END -----------
|
||||
|
||||
if sys.argv[1] == 'bulkdeletecards':
|
||||
# ------- BULK DELETE CARDS START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py bulkdeletecards BOARDID CARDIDS")
|
||||
print("CARDIDS = comma-separated cardIds")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
cardids_raw = sys.argv[3]
|
||||
cardids = [x for x in cardids_raw.split(',') if x]
|
||||
bulk_url = wekanurl + apiboards + boardid + s + cs + s + 'bulk'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
# requests sends a body with DELETE via the json= parameter.
|
||||
body = requests.delete(bulk_url, json={'cardIds': cardids}, headers=headers)
|
||||
print("=== BULK DELETE CARDS ===\n")
|
||||
print(body.text)
|
||||
# ------- BULK DELETE CARDS END -----------
|
||||
|
||||
if sys.argv[1] == 'bulkcardlabels':
|
||||
# ------- BULK CARD LABELS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS")
|
||||
print("CARDIDS / ADDLABELIDS / REMOVELABELIDS = comma-separated, or '' for none")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
cardids = [x for x in sys.argv[3].split(',') if x]
|
||||
add_labelids = [x for x in sys.argv[4].split(',') if x]
|
||||
remove_labelids = [x for x in sys.argv[5].split(',') if x]
|
||||
bulk_url = wekanurl + apiboards + boardid + s + cs + s + 'labels'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'cardIds': cardids, 'addLabelIds': add_labelids, 'removeLabelIds': remove_labelids}
|
||||
body = requests.post(bulk_url, json=post_data, headers=headers)
|
||||
print("=== BULK CARD LABELS ===\n")
|
||||
print(body.text)
|
||||
# ------- BULK CARD LABELS END -----------
|
||||
|
||||
if sys.argv[1] == 'linkcard':
|
||||
# ------- CREATE LINKED CARD START -----------
|
||||
if arguments < 6:
|
||||
print("Usage: python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
swimlaneid = sys.argv[4]
|
||||
authorid = sys.argv[5]
|
||||
linkedcardid = sys.argv[6]
|
||||
link_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'linkedId': linkedcardid}
|
||||
body = requests.post(link_url, data=post_data, headers=headers)
|
||||
print("=== CREATE LINKED CARD ===\n")
|
||||
print(body.text)
|
||||
# ------- CREATE LINKED CARD END -----------
|
||||
|
||||
if sys.argv[1] in ('addcardmember', 'removecardmember', 'addcardassignee', 'removecardassignee'):
|
||||
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py {} BOARDID LISTID CARDID USERID".format(sys.argv[1]))
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
userid = sys.argv[5]
|
||||
field = 'members' if 'member' in sys.argv[1] else 'assignees'
|
||||
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + field + s + userid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
if sys.argv[1].startswith('add'):
|
||||
body = requests.post(url, headers=headers)
|
||||
else:
|
||||
body = requests.delete(url, headers=headers)
|
||||
print("=== {} ===\n".format(sys.argv[1].upper()))
|
||||
print(body.text)
|
||||
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE END -----------
|
||||
|
||||
if sys.argv[1] == 'copycard':
|
||||
# ------- COPY CARD START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION]")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
toswimlaneid = sys.argv[5] if arguments > 4 else None
|
||||
if not toswimlaneid:
|
||||
print("TOSWIMLANEID is required")
|
||||
exit(1)
|
||||
toboardid = sys.argv[6] if arguments > 5 else boardid
|
||||
tolistid = sys.argv[7] if arguments > 6 else listid
|
||||
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + 'copy'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'toBoardId': toboardid, 'toSwimlaneId': toswimlaneid, 'toListId': tolistid}
|
||||
if arguments > 7:
|
||||
post_data['position'] = int(sys.argv[8])
|
||||
body = requests.post(url, json=post_data, headers=headers)
|
||||
print("=== COPY CARD ===\n")
|
||||
print(body.text)
|
||||
# ------- COPY CARD END -----------
|
||||
|
||||
if sys.argv[1] in ('copyswimlane', 'moveswimlane'):
|
||||
# ------- COPY/MOVE SWIMLANE START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py {} BOARDID SWIMLANEID [TOBOARDID] [POSITION]".format(sys.argv[1]))
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
toboardid = sys.argv[4] if arguments > 3 else boardid
|
||||
action = 'copy' if sys.argv[1] == 'copyswimlane' else 'move'
|
||||
url = wekanurl + apiboards + boardid + s + sws + s + swimlaneid + s + action
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'toBoardId': toboardid}
|
||||
if arguments > 4:
|
||||
post_data['position'] = int(sys.argv[5])
|
||||
body = requests.post(url, json=post_data, headers=headers)
|
||||
print("=== {} ===\n".format(sys.argv[1].upper()))
|
||||
print(body.text)
|
||||
# ------- COPY/MOVE SWIMLANE END -----------
|
||||
|
||||
if sys.argv[1] in ('copylist', 'movelist'):
|
||||
# ------- COPY/MOVE LIST START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py {} BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION]".format(sys.argv[1]))
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
toswimlaneid = sys.argv[4] if arguments > 3 else None
|
||||
toboardid = sys.argv[5] if arguments > 4 else boardid
|
||||
action = 'copy' if sys.argv[1] == 'copylist' else 'move'
|
||||
url = wekanurl + apiboards + boardid + s + l + s + listid + s + action
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'toBoardId': toboardid}
|
||||
if toswimlaneid:
|
||||
post_data['toSwimlaneId'] = toswimlaneid
|
||||
if arguments > 5:
|
||||
post_data['position'] = int(sys.argv[6])
|
||||
body = requests.post(url, json=post_data, headers=headers)
|
||||
print("=== {} ===\n".format(sys.argv[1].upper()))
|
||||
print(body.text)
|
||||
# ------- COPY/MOVE LIST END -----------
|
||||
|
||||
if sys.argv[1] == 'mycards':
|
||||
# ------- MY CARDS / DUE CARDS START -----------
|
||||
params = {}
|
||||
if arguments >= 2 and sys.argv[2].lower() == 'due':
|
||||
params['due'] = 'true'
|
||||
if arguments >= 3:
|
||||
params['from'] = sys.argv[3]
|
||||
if arguments >= 4:
|
||||
params['to'] = sys.argv[4]
|
||||
url = wekanurl + 'api/user/cards'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
body = requests.get(url, headers=headers, params=params)
|
||||
print("=== MY CARDS ===\n")
|
||||
data2 = body.text.replace('}', "}\n")
|
||||
print(data2)
|
||||
# ------- MY CARDS / DUE CARDS END -----------
|
||||
|
||||
if sys.argv[1] == 'getcardsettings':
|
||||
# ------- GET CARD SETTINGS START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py getcardsettings BOARDID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
url = wekanurl + apiboards + boardid + s + 'cardSettings'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
body = requests.get(url, headers=headers)
|
||||
print("=== CARD SETTINGS ===\n")
|
||||
data2 = body.text.replace(',', ",\n")
|
||||
print(data2)
|
||||
# ------- GET CARD SETTINGS END -----------
|
||||
|
||||
if sys.argv[1] == 'setcardsetting':
|
||||
# ------- SET CARD SETTING START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py setcardsetting BOARDID KEY VALUE")
|
||||
print("e.g. python3 api.py setcardsetting BOARDID allowsDueDate true")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
key = sys.argv[3]
|
||||
value = sys.argv[4]
|
||||
url = wekanurl + apiboards + boardid + s + 'cardSettings'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
body = requests.put(url, json={key: value}, headers=headers)
|
||||
print("=== SET CARD SETTING ===\n")
|
||||
print(body.text)
|
||||
# ------- SET CARD SETTING END -----------
|
||||
|
||||
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS END -----------
|
||||
@@ -8,10 +8,10 @@ template(name="archivedBoards")
|
||||
each archivedBoards
|
||||
li.archived-lists-item
|
||||
div.board-header-btns
|
||||
button.board-header-btn.js-delete-board
|
||||
button.primary.board-header-btn.js-delete-board
|
||||
i.fa.fa-trash
|
||||
| {{_ 'delete-board'}}
|
||||
button.board-header-btn.js-restore-board
|
||||
button.primary.board-header-btn.js-restore-board
|
||||
i.fa.fa-undo
|
||||
| {{_ 'restore-board'}}
|
||||
= title
|
||||
|
||||
@@ -55,11 +55,10 @@ template(name="cardCustomField-number")
|
||||
template(name="cardCustomField-checkbox")
|
||||
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if data.value}}is-checked{{/if}}")
|
||||
else
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.materialCheckBox(class="{{#if data.value}}is-checked{{/if}}")
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
|
||||
@@ -106,9 +106,6 @@ Template['cardCustomField-checkbox'].onCreated(function () {
|
||||
});
|
||||
|
||||
Template['cardCustomField-checkbox'].events({
|
||||
'click .js-checklist-item .check-box-unicode'(event, tpl) {
|
||||
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
|
||||
},
|
||||
'click .js-checklist-item .check-box-container'(event, tpl) {
|
||||
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
|
||||
},
|
||||
|
||||
@@ -890,3 +890,167 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
||||
transform: scale(1.01);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Sticker picker + card-detail stickers + due-complete toggle */
|
||||
.card-stickers-picker {
|
||||
max-width: 320px;
|
||||
}
|
||||
.card-sticker-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 2px;
|
||||
border-radius: 3px;
|
||||
font-size: 18px;
|
||||
color: #4d4d4d;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.card-sticker-select:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.card-sticker-select.active {
|
||||
background: #d6ecd2;
|
||||
color: #3cb500;
|
||||
}
|
||||
.card-sticker-select .card-sticker-check {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
right: 2px;
|
||||
font-size: 10px;
|
||||
color: #3cb500;
|
||||
}
|
||||
.card-stickers .card-sticker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 8px 4px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.card-sticker .card-sticker-remove {
|
||||
margin-left: 3px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-sticker .card-sticker-remove:hover {
|
||||
color: #eb4646;
|
||||
}
|
||||
.card-due-complete-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Trello-style "mark complete" toggle, left of the card title */
|
||||
.card-details-complete-toggle {
|
||||
float: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Title is 1.35em with 1.3 line-height (~28px box) and margin-top 7px; push
|
||||
the 13px checkbox down so it lands at the vertical middle of the title. */
|
||||
margin: 14px 10px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Card locations: multiple locations, each editable with a remove button */
|
||||
.card-details-item-location .card-locations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.card-details-item-location .card-location {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.card-details-item-location .card-location-info {
|
||||
flex: 1;
|
||||
}
|
||||
.card-details-item-location .js-edit-location {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-details-item-location .card-location-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.card-details-item-location .card-location-address {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
.card-details-item-location .card-location-remove {
|
||||
margin-left: 8px;
|
||||
color: #8c8c8c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-details-item-location .card-location-remove:hover {
|
||||
color: #eb4646;
|
||||
}
|
||||
|
||||
/* Sticker icon highlight styles (used instead of colour): taco pack => underline,
|
||||
pete pack => round (ring around the icon). Icon keeps its normal colour. */
|
||||
.card-sticker i.sticker-underline {
|
||||
border-bottom: 2px solid currentColor;
|
||||
padding-bottom: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
.card-sticker i.sticker-round {
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* "Open map links at" provider selector in the location popup */
|
||||
.js-card-location-form .map-provider-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.js-card-location-form .map-provider-row .js-map-provider {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Opened card (card details popup) docks to the very top of the window and
|
||||
overlays the global + board header bars instead of opening below them. The
|
||||
base .pop-over is position:absolute with a small margin/top offset, which left
|
||||
the card's top behind the fixed header bars; fix it to the viewport top with a
|
||||
z-index above every header (headers top out around z-index 1000-2000). */
|
||||
.pop-over[data-popup="cardDetailsPopup"] {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
/* Sticker highlight styles inside the sticker picker (mascot = underline,
|
||||
computer = round), matching how they render on the card. */
|
||||
.card-sticker-select i.sticker-underline {
|
||||
border-bottom: 2px solid currentColor;
|
||||
padding-bottom: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
.card-sticker-select i.sticker-round {
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -61,6 +61,9 @@ template(name="cardDetails")
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
if canModifyCard
|
||||
a.card-details-complete-toggle.js-toggle-due-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
|
||||
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
|
||||
h2.card-details-title.js-card-title(
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
|
||||
+viewer
|
||||
@@ -126,6 +129,47 @@ template(name="cardDetails")
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-item.card-details-item-stickers
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sticky-note-o
|
||||
| {{_ 'stickers'}}
|
||||
.card-details-item-content.card-stickers
|
||||
each stickers
|
||||
span.card-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if canModifyCard
|
||||
a.card-sticker-remove.js-remove-sticker(data-index="{{@index}}" title="{{_ 'remove-sticker'}}")
|
||||
i.fa.fa-times
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-stickers(title="{{_ 'add-sticker'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-item.card-details-item-location
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-map-marker
|
||||
| {{_ 'location'}}
|
||||
.card-details-item-content.card-locations
|
||||
each getLocations
|
||||
.card-location
|
||||
.card-location-info(class="{{#if canModifyCard}}js-edit-location{{/if}}" data-location-id="{{_id}}")
|
||||
if name
|
||||
.card-location-name= name
|
||||
if address
|
||||
.card-location-address= address
|
||||
if hasCoordinates
|
||||
a.card-location-map(href=mapUrl target="_blank" rel="noopener noreferrer")
|
||||
i.fa.fa-map-o
|
||||
= ' '
|
||||
| {{_ 'location-open-map'}}
|
||||
if canModifyCard
|
||||
a.card-location-remove.js-remove-location(data-location-id="{{_id}}" title="{{_ 'delete'}}")
|
||||
i.fa.fa-times
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-location(title="{{_ 'add-location'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
hr
|
||||
|
||||
@@ -1196,3 +1240,39 @@ template(name="cardStartPlanningPokerPopup")
|
||||
if getPokerQuestion
|
||||
if currentUser.isBoardAdmin
|
||||
button.js-remove-poker.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="cardLocationsPopup")
|
||||
form.js-card-location-form
|
||||
label
|
||||
| {{_ 'location-detect-from-map'}}
|
||||
input.js-location-map-link(type="text" placeholder="https://..." autofocus)
|
||||
button.primary.js-detect-location(type="button") {{_ 'location-detect'}}
|
||||
if detectMessage
|
||||
p.quiet.js-detect-message {{detectMessage}}
|
||||
hr
|
||||
label
|
||||
| {{_ 'location-name'}}
|
||||
input.js-location-name(type="text" value="{{location.name}}")
|
||||
label
|
||||
| {{_ 'location-address'}}
|
||||
input.js-location-address(type="text" value="{{location.address}}")
|
||||
label
|
||||
| {{_ 'location-latitude'}}
|
||||
input.js-location-latitude(type="number" step="any" value="{{location.latitude}}")
|
||||
label
|
||||
| {{_ 'location-longitude'}}
|
||||
input.js-location-longitude(type="number" step="any" value="{{location.longitude}}")
|
||||
button.primary.confirm.js-submit-location(type="submit") {{_ 'save'}}
|
||||
if location._id
|
||||
button.negate.wide.right.js-delete-location(type="button") {{_ 'delete'}}
|
||||
hr
|
||||
.map-provider-row
|
||||
span.map-provider-label {{_ 'location-open-map-at'}}
|
||||
select.js-map-provider
|
||||
option(value="openstreetmap" selected="{{#if isMapProvider 'openstreetmap'}}selected{{/if}}") OpenStreetMap
|
||||
option(value="google" selected="{{#if isMapProvider 'google'}}selected{{/if}}") Google Maps
|
||||
option(value="bing" selected="{{#if isMapProvider 'bing'}}selected{{/if}}") Bing Maps
|
||||
option(value="apple" selected="{{#if isMapProvider 'apple'}}selected{{/if}}") Apple Maps
|
||||
button.primary.js-save-map-provider(type="button") {{_ 'save'}}
|
||||
if mapSavedMessage
|
||||
p.quiet.js-map-saved {{mapSavedMessage}}
|
||||
@@ -51,8 +51,100 @@ import { UnsavedEdits } from '/client/lib/unsavedEdits';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import autosize from 'autosize';
|
||||
|
||||
// Id of the location currently being edited in the cardLocationsPopup; null
|
||||
// when adding a new location.
|
||||
const editingLocationId = new ReactiveVar(null);
|
||||
|
||||
// Parse coordinates (and, when present, a place name / address) out of a map
|
||||
// link from common providers: Google Maps, OpenStreetMap, Bing Maps, Apple Maps
|
||||
// and generic `?q=lat,lon` / `?ll=lat,lon` links. Returns whatever it can find
|
||||
// as { latitude, longitude, name, address }.
|
||||
function parseMapLink(url) {
|
||||
const result = {};
|
||||
if (!url) return result;
|
||||
const s = String(url).trim();
|
||||
|
||||
let lat;
|
||||
let lon;
|
||||
const setCoords = (la, lo) => {
|
||||
if (lat !== undefined) return;
|
||||
const a = parseFloat(la);
|
||||
const o = parseFloat(lo);
|
||||
if (!isNaN(a) && !isNaN(o) && Math.abs(a) <= 90 && Math.abs(o) <= 180) {
|
||||
lat = a;
|
||||
lon = o;
|
||||
}
|
||||
};
|
||||
|
||||
const N = '(-?\\d+(?:\\.\\d+)?)';
|
||||
let m;
|
||||
// Google Maps place marker in the data part: !3d<lat>!4d<lon> (most precise).
|
||||
if ((m = s.match(new RegExp(`!3d${N}!4d${N}`)))) setCoords(m[1], m[2]);
|
||||
// Google Maps view/place: @<lat>,<lon>
|
||||
if ((m = s.match(new RegExp(`@${N},${N}`)))) setCoords(m[1], m[2]);
|
||||
// OpenStreetMap marker: ?mlat=..&mlon=..
|
||||
if ((m = s.match(new RegExp(`[?&#]mlat=${N}`)))) {
|
||||
const mlon = s.match(new RegExp(`[?&#]mlon=${N}`));
|
||||
if (mlon) setCoords(m[1], mlon[1]);
|
||||
}
|
||||
// OpenStreetMap map hash: #map=z/lat/lon
|
||||
if ((m = s.match(new RegExp(`[#&]map=\\d+(?:\\.\\d+)?/${N}/${N}`)))) setCoords(m[1], m[2]);
|
||||
// Bing Maps: cp=lat~lon
|
||||
if ((m = s.match(new RegExp(`[?&]cp=${N}~${N}`)))) setCoords(m[1], m[2]);
|
||||
// Apple Maps and generic ll=lat,lon
|
||||
if ((m = s.match(new RegExp(`[?&]ll=${N},${N}`)))) setCoords(m[1], m[2]);
|
||||
// Generic q=lat,lon (Google/Apple query form)
|
||||
if ((m = s.match(new RegExp(`[?&]q=${N},${N}`)))) setCoords(m[1], m[2]);
|
||||
// Generic center=lat,lon
|
||||
if ((m = s.match(new RegExp(`[?&]center=${N},${N}`)))) setCoords(m[1], m[2]);
|
||||
|
||||
if (lat !== undefined) {
|
||||
result.latitude = lat;
|
||||
result.longitude = lon;
|
||||
}
|
||||
|
||||
const decode = (raw) => {
|
||||
try {
|
||||
return decodeURIComponent(raw.replace(/\+/g, ' ')).trim();
|
||||
} catch (e) {
|
||||
return raw.replace(/\+/g, ' ').trim();
|
||||
}
|
||||
};
|
||||
const isCoordText = (t) => /^-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?$/.test(t);
|
||||
|
||||
// Place name from Google Maps /place/<Name>/
|
||||
if ((m = s.match(/\/place\/([^/@?]+)/))) {
|
||||
const name = decode(m[1]);
|
||||
if (name && !isCoordText(name)) result.name = name;
|
||||
}
|
||||
// Address from a textual q=/query=/destination= parameter.
|
||||
if ((m = s.match(/[?&](?:q|query|destination)=([^&]+)/))) {
|
||||
const q = decode(m[1]);
|
||||
if (q && !isCoordText(q)) result.address = q;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build an "open in map" link for the given provider and coordinates. Mirrors
|
||||
// the providers offered in the location popup's "Open map links at" selector.
|
||||
function mapLinkFor(provider, lat, lon) {
|
||||
switch (provider) {
|
||||
case 'google':
|
||||
return `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`;
|
||||
case 'bing':
|
||||
return `https://www.bing.com/maps?cp=${lat}~${lon}&lvl=16`;
|
||||
case 'apple':
|
||||
return `https://maps.apple.com/?ll=${lat},${lon}`;
|
||||
case 'openstreetmap':
|
||||
default:
|
||||
return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`;
|
||||
}
|
||||
}
|
||||
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
@@ -121,9 +213,24 @@ Template.cardDetails.onCreated(function () {
|
||||
};
|
||||
|
||||
Meteor.subscribe('unsaved-edits');
|
||||
|
||||
// Surface legacy CollectionFS attachments for the current board so they show
|
||||
// up in the card's attachment gallery and can be read in place (without
|
||||
// migrating them to Meteor-Files first).
|
||||
this.autorun(() => {
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (board && board._id) {
|
||||
Meteor.subscribe('legacyBoardAttachments', board._id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardDetails.onRendered(function () {
|
||||
// A reactive re-render (e.g. a card moving between the inline swimlane render
|
||||
// and the draggable openCards popup) can create and then remove this instance
|
||||
// within the same flush. If our DOM range is already gone, calling this.$()
|
||||
// below throws "Can't select in removed DomRange", so bail out early.
|
||||
if (this.view && this.view.isDestroyed) return;
|
||||
this.calculateNextPeak();
|
||||
if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
|
||||
// Send Webhook but not create Activities records ---
|
||||
@@ -263,6 +370,23 @@ Template.cardDetails.helpers({
|
||||
return card.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
// Returns the card's locations (multiple supported), each enriched with the
|
||||
// coordinate flag and OpenStreetMap link used by the template.
|
||||
getLocations() {
|
||||
const card = Template.currentData();
|
||||
if (!card || !card.getLocations) return [];
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const provider = user ? user.getMapProvider() : 'openstreetmap';
|
||||
return card.getLocations().map(loc => {
|
||||
const hasCoordinates =
|
||||
typeof loc.latitude === 'number' && typeof loc.longitude === 'number';
|
||||
const mapUrl = hasCoordinates
|
||||
? mapLinkFor(provider, loc.latitude, loc.longitude)
|
||||
: '';
|
||||
return { ...loc, hasCoordinates, mapUrl };
|
||||
});
|
||||
},
|
||||
|
||||
customFieldsGrid() {
|
||||
return ReactiveCache.getCurrentUser().hasCustomFieldsGrid();
|
||||
},
|
||||
@@ -600,9 +724,57 @@ Template.cardDetails.events({
|
||||
'click .js-assignee': Popup.open('cardAssignee'),
|
||||
'click .js-add-assignees': Popup.open('cardAssignees'),
|
||||
'click .js-add-labels': Popup.open('cardLabels'),
|
||||
'click .js-add-stickers': Popup.open('cardStickers'),
|
||||
'click .js-remove-sticker'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!Utils.canModifyCard()) return;
|
||||
const index = parseInt(event.currentTarget.dataset.index, 10);
|
||||
const card = Template.currentData();
|
||||
if (card && !Number.isNaN(index)) {
|
||||
card.removeStickerAt(index);
|
||||
}
|
||||
},
|
||||
'click .js-add-location'(event) {
|
||||
event.preventDefault();
|
||||
if (!Utils.canModifyCard()) return;
|
||||
const card = Template.currentData();
|
||||
editingLocationId.set(null);
|
||||
Popup.open('cardLocations')(event, {
|
||||
dataContextIfCurrentDataIsUndefined: card,
|
||||
});
|
||||
},
|
||||
'click .js-edit-location'(event) {
|
||||
// Let the "Open in map" link work without also opening the edit popup.
|
||||
if ($(event.target).closest('a.card-location-map').length) return;
|
||||
event.preventDefault();
|
||||
if (!Utils.canModifyCard()) return;
|
||||
const card = Template.currentData();
|
||||
editingLocationId.set(event.currentTarget.dataset.locationId || null);
|
||||
Popup.open('cardLocations')(event, {
|
||||
dataContextIfCurrentDataIsUndefined: card,
|
||||
});
|
||||
},
|
||||
'click .js-remove-location'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!Utils.canModifyCard()) return;
|
||||
const locationId = event.currentTarget.dataset.locationId;
|
||||
const card = Template.currentData();
|
||||
if (card && locationId) {
|
||||
card.removeLocation(locationId);
|
||||
}
|
||||
},
|
||||
'click .js-received-date': Popup.open('editCardReceivedDate'),
|
||||
'click .js-start-date': Popup.open('editCardStartDate'),
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-toggle-due-complete'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!Utils.canModifyCard()) return;
|
||||
const card = Template.currentData();
|
||||
card.setDueComplete(!card.getDueComplete());
|
||||
},
|
||||
'click .js-end-date': Popup.open('editCardEndDate'),
|
||||
'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
|
||||
'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
|
||||
@@ -1134,6 +1306,109 @@ Template.cardMembersPopup.helpers({
|
||||
},
|
||||
});
|
||||
|
||||
// Popup that adds or edits a single card location (name, address, latitude,
|
||||
// longitude). Cards can hold multiple locations, like members.
|
||||
Template.cardLocationsPopup.onCreated(function () {
|
||||
const data = Template.currentData();
|
||||
this.cardId = data && data._id;
|
||||
this.detectMsg = new ReactiveVar('');
|
||||
this.mapSavedMsg = new ReactiveVar('');
|
||||
});
|
||||
|
||||
Template.cardLocationsPopup.helpers({
|
||||
location() {
|
||||
const tpl = Template.instance();
|
||||
const card = ReactiveCache.getCard(tpl.cardId);
|
||||
const id = editingLocationId.get();
|
||||
if (card && id) {
|
||||
const found = card.getLocations().find(loc => loc._id === id);
|
||||
if (found) return found;
|
||||
}
|
||||
return {};
|
||||
},
|
||||
detectMessage() {
|
||||
return Template.instance().detectMsg.get();
|
||||
},
|
||||
isMapProvider(provider) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const current = user ? user.getMapProvider() : 'openstreetmap';
|
||||
return current === provider;
|
||||
},
|
||||
mapSavedMessage() {
|
||||
return Template.instance().mapSavedMsg.get();
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardLocationsPopup.events({
|
||||
'click .js-detect-location'(event) {
|
||||
event.preventDefault();
|
||||
const tpl = Template.instance();
|
||||
const linkInput = tpl.find('.js-location-map-link');
|
||||
const parsed = parseMapLink(linkInput ? linkInput.value : '');
|
||||
let filled = false;
|
||||
if (typeof parsed.latitude === 'number') {
|
||||
tpl.find('.js-location-latitude').value = parsed.latitude;
|
||||
tpl.find('.js-location-longitude').value = parsed.longitude;
|
||||
filled = true;
|
||||
}
|
||||
if (parsed.name) {
|
||||
tpl.find('.js-location-name').value = parsed.name;
|
||||
filled = true;
|
||||
}
|
||||
if (parsed.address) {
|
||||
tpl.find('.js-location-address').value = parsed.address;
|
||||
filled = true;
|
||||
}
|
||||
tpl.detectMsg.set(
|
||||
TAPi18n.__(filled ? 'location-detect-done' : 'location-detect-none'),
|
||||
);
|
||||
},
|
||||
'submit .js-card-location-form'(event) {
|
||||
event.preventDefault();
|
||||
const tpl = Template.instance();
|
||||
const card = ReactiveCache.getCard(tpl.cardId);
|
||||
if (!card) {
|
||||
Popup.back();
|
||||
return;
|
||||
}
|
||||
const name = tpl.find('.js-location-name').value.trim();
|
||||
const address = tpl.find('.js-location-address').value.trim();
|
||||
const latRaw = tpl.find('.js-location-latitude').value.trim();
|
||||
const lonRaw = tpl.find('.js-location-longitude').value.trim();
|
||||
const latitude = latRaw === '' ? undefined : parseFloat(latRaw);
|
||||
const longitude = lonRaw === '' ? undefined : parseFloat(lonRaw);
|
||||
const data = { name, address, latitude, longitude };
|
||||
const id = editingLocationId.get();
|
||||
if (id) {
|
||||
card.updateLocation(id, data);
|
||||
} else {
|
||||
card.addLocation(data);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-delete-location'(event) {
|
||||
event.preventDefault();
|
||||
const tpl = Template.instance();
|
||||
const card = ReactiveCache.getCard(tpl.cardId);
|
||||
const id = editingLocationId.get();
|
||||
if (card && id) {
|
||||
card.removeLocation(id);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-save-map-provider'(event) {
|
||||
event.preventDefault();
|
||||
const tpl = Template.instance();
|
||||
const select = tpl.find('.js-map-provider');
|
||||
const provider = select ? select.value : 'openstreetmap';
|
||||
Meteor.call('setMapProvider', provider, err => {
|
||||
tpl.mapSavedMsg.set(
|
||||
TAPi18n.__(err ? 'server-error' : 'map-provider-saved'),
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const filterMembers = (filterTerm) => {
|
||||
let currBoard = Utils.getCurrentBoard();
|
||||
let members = currBoard.activeMembers();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
template(name="cardStickersPopup")
|
||||
.card-stickers-picker.clearfix
|
||||
each stickerIcons
|
||||
a.card-sticker-select.js-select-sticker(class="{{#if isSelected}}active{{/if}}" title="{{stickerTitle}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if isSelected
|
||||
i.fa.fa-check.card-sticker-check
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { STICKER_PICKER } from '/models/metadata/stickers';
|
||||
|
||||
// Picker popup that lets a user add/remove stickers on a card. Each entry is
|
||||
// { icon, highlight?, name? }: the plain Font Awesome icons plus the mascot
|
||||
// (underlined) and computer (ringed) highlighted stickers. Opened from the card
|
||||
// details stickers section with the card as the data context.
|
||||
|
||||
Template.cardStickersPopup.onCreated(function () {
|
||||
const data = Template.currentData();
|
||||
this.cardId = data && data._id;
|
||||
});
|
||||
|
||||
function pickerCard(tpl) {
|
||||
return ReactiveCache.getCard(tpl.cardId);
|
||||
}
|
||||
|
||||
Template.cardStickersPopup.helpers({
|
||||
stickerIcons() {
|
||||
return STICKER_PICKER;
|
||||
},
|
||||
isSelected() {
|
||||
const card = pickerCard(Template.instance());
|
||||
return !!(card && card.hasSticker(this.icon, this.highlight));
|
||||
},
|
||||
stickerTitle() {
|
||||
return this.name || this.icon;
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardStickersPopup.events({
|
||||
'click .js-select-sticker'(event) {
|
||||
event.preventDefault();
|
||||
const card = pickerCard(Template.instance());
|
||||
if (card) {
|
||||
card.toggleSticker(this.icon, this.highlight, this.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -28,6 +28,18 @@
|
||||
.minicard-wrapper .multi-selection-checkbox + .minicard {
|
||||
margin-left: 1vw;
|
||||
}
|
||||
/* Enlarge the clickable area of the small selection checkbox so it can be
|
||||
reliably clicked at the left side of the minicard. The visible 13px box (and
|
||||
its rotated is-checked checkmark) keep their size; an invisible overlay only
|
||||
extends the hit target. */
|
||||
.minicard-wrapper .multi-selection-checkbox::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
right: -8px;
|
||||
bottom: -8px;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard {
|
||||
padding: 8px 1vw 3px;
|
||||
@@ -822,3 +834,35 @@
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Trello-style "mark complete" toggle, left of the minicard title */
|
||||
.minicard-title .minicard-complete-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
/* Nudge down so the checkmark sits at the vertical middle of the title text
|
||||
(the checked materialCheckBox is shifted up 4px by its own positioning). */
|
||||
position: relative;
|
||||
top: 4px;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Sticker icon highlight styles (used instead of colour): taco pack => underline,
|
||||
pete pack => round (ring around the icon). Icon keeps its normal colour. */
|
||||
.minicard-sticker i.sticker-underline {
|
||||
border-bottom: 2px solid currentColor;
|
||||
padding-bottom: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
.minicard-sticker i.sticker-round {
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ template(name="minicard")
|
||||
+minicardStartDate
|
||||
if showDue
|
||||
if getDue
|
||||
.date
|
||||
.date(class="{{#if getDueComplete}}due-complete{{/if}}")
|
||||
+minicardDueDate
|
||||
if showEnd
|
||||
if getEnd
|
||||
@@ -54,6 +54,9 @@ template(name="minicard")
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.minicard-title
|
||||
if canModifyCard
|
||||
a.minicard-complete-toggle.js-toggle-card-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
|
||||
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
|
||||
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
| {{ parentString ' > ' }}
|
||||
@@ -152,6 +155,11 @@ template(name="minicard")
|
||||
each shouldShowChecklistAtMinicard
|
||||
+minicardChecklist(checklist=. card=..)
|
||||
.badges
|
||||
if stickers.length
|
||||
.badge.badge-state-image-only.minicard-stickers
|
||||
each stickers
|
||||
span.badge-icon.minicard-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if comments.length
|
||||
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
||||
span.badge-icon.badge-comment.badge-text
|
||||
|
||||
@@ -178,6 +178,16 @@ Template.minicard.events({
|
||||
else if (this.isLinkedBoard())
|
||||
Utils.goBoardId(this.linkedId);
|
||||
},
|
||||
'click .js-toggle-card-complete'(event) {
|
||||
// Trello-style "mark complete" toggle (left of the title). Don't let the
|
||||
// click open the card.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!Utils.canModifyCard()) {
|
||||
return;
|
||||
}
|
||||
this.setDueComplete(!this.getDueComplete());
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.minicard-title .minicard-complete-toggle {
|
||||
display: none;
|
||||
}
|
||||
@@ -139,6 +139,9 @@ textarea.js-edit-subtask-item {
|
||||
.subtasks-item:hover {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
.subtasks-item .check-box-container {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.subtasks-item .check-box {
|
||||
margin: 0.1em 0 0 0;
|
||||
}
|
||||
|
||||
@@ -74,14 +74,13 @@ template(name="subtasksItems")
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished}}is-checked{{/if}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.materialCheckBox(class="{{#if item.isFinished}}is-checked{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
||||
@@ -119,7 +119,7 @@ Template.subtasks.helpers({
|
||||
});
|
||||
|
||||
Template.subtaskItemDetail.events({
|
||||
async 'click .js-subtasks-item .check-box-unicode'() {
|
||||
async 'click .js-subtasks-item .check-box-container'() {
|
||||
const item = Template.currentData().item;
|
||||
if (item && item._id) {
|
||||
await item.toggleItem();
|
||||
|
||||
Vendored
+116
@@ -49,3 +49,119 @@ a.show-mapping {
|
||||
font-size: 90%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Trello API import: file rows, workspace tree, job progress and errors */
|
||||
/* The shared page spinners are centered with a large top margin (e.g. bounce
|
||||
uses margin:100px auto), which leaves a big empty gap above the spinner in
|
||||
this compact panel. Make spinners here small and inline instead. */
|
||||
.trello-api-import .sk-spinner {
|
||||
margin: 0 8px 0 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
}
|
||||
.trello-api-import .import-file-row label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
.trello-api-import .import-file-row input,
|
||||
.trello-api-import .import-file-row select {
|
||||
width: 100%;
|
||||
}
|
||||
.trello-workspaces {
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.trello-workspace {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.trello-workspace .trello-boards {
|
||||
margin-left: 22px;
|
||||
}
|
||||
.trello-workspace-title,
|
||||
.trello-board {
|
||||
align-items: center;
|
||||
padding: 3px 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.trello-workspace-title .materialCheckBox,
|
||||
.trello-board .materialCheckBox {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.trello-select-controls button {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.trello-job-controls button {
|
||||
margin-right: 6px;
|
||||
}
|
||||
/* Import progress meter, styled like Admin Panel / Attachments / Move Attachments */
|
||||
.trello-progress-label {
|
||||
margin: 8px 0 6px;
|
||||
}
|
||||
.trello-progress-bar {
|
||||
background: #e9ecef;
|
||||
border-radius: 8px;
|
||||
height: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.trello-progress-fill {
|
||||
background: linear-gradient(90deg, #28a745, #20c997);
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
.trello-progress-fill.is-running::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||
animation: trelloProgressShimmer 2s infinite;
|
||||
}
|
||||
@keyframes trelloProgressShimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
.trello-progress-percent {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.trello-import-result.is-ok {
|
||||
color: #3cb500;
|
||||
}
|
||||
.trello-import-result.is-error {
|
||||
color: #eb4646;
|
||||
}
|
||||
.js-trello-errors-text {
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Trello API credential Save/Delete buttons */
|
||||
.trello-api-creds-buttons button {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.trello-creds-saved {
|
||||
color: #3cb500;
|
||||
}
|
||||
|
||||
/* Simplified per-field hint text under the Trello file upload labels */
|
||||
.import-file-row .import-file-hint {
|
||||
display: block;
|
||||
font-size: 0.85em;
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ template(name="import")
|
||||
.wrapper
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
if zipImporting
|
||||
.warning {{_ 'import-trello-zip-progress'}}
|
||||
+Template.dynamic(template=currentTemplate)
|
||||
|
||||
template(name="importTextarea")
|
||||
@@ -15,7 +17,115 @@ template(name="importTextarea")
|
||||
p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
|
||||
textarea.js-import-json(id='import-textarea' placeholder="{{_ importPlaceHolder}}" autofocus)
|
||||
| {{jsonText}}
|
||||
if isTrelloImport
|
||||
p.import-file-row
|
||||
label(for='import-json-file') {{_ 'import-trello-json-file'}}
|
||||
span.quiet.import-file-hint {{_ 'import-trello-json-file-hint'}}
|
||||
input.js-import-json-file(id='import-json-file' type="file" accept=".json,application/json")
|
||||
p.import-file-row
|
||||
label(for='import-zip-file') {{_ 'import-trello-zip-file'}}
|
||||
span.quiet.import-file-hint {{_ 'import-trello-zip-file-hint'}}
|
||||
input.js-import-zip-file(id='import-zip-file' type="file" accept=".zip,application/zip")
|
||||
p.import-file-row
|
||||
label(for='import-workspace-name') {{_ 'import-trello-workspace'}}
|
||||
input.js-import-workspace-name(id='import-workspace-name' type="text" placeholder="{{_ 'import-trello-workspace-placeholder'}}")
|
||||
input.primary.wide(type="submit" value="{{_ 'import'}}")
|
||||
if isTrelloImport
|
||||
hr
|
||||
+importTrelloApi
|
||||
|
||||
template(name="importTrelloApi")
|
||||
.trello-api-import
|
||||
h2 {{_ 'trello-api-import'}}
|
||||
p.quiet {{_ 'trello-api-import-desc'}}
|
||||
p.import-file-row
|
||||
label(for='trello-api-key') {{_ 'trello-api-key'}}
|
||||
input.js-trello-key(id='trello-api-key' type="text" autocomplete="off")
|
||||
p.import-file-row
|
||||
label(for='trello-api-token') {{_ 'trello-api-token'}}
|
||||
input.js-trello-token(id='trello-api-token' type="password" autocomplete="off")
|
||||
p.trello-api-creds-buttons
|
||||
button.primary.js-trello-save-creds(type="button") {{_ 'save'}}
|
||||
button.negate.js-trello-delete-creds(type="button") {{_ 'delete'}}
|
||||
if credsSaved
|
||||
p.quiet.trello-creds-saved
|
||||
i.fa.fa-check
|
||||
span {{_ 'trello-api-credentials-saved'}}
|
||||
p
|
||||
button.primary.js-trello-list-workspaces(type="button") {{_ 'trello-list-workspaces'}}
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
if loading.get
|
||||
+spinner
|
||||
if hasWorkspaces
|
||||
p.import-file-row
|
||||
label(for='trello-parent-workspace') {{_ 'import-trello-parent-workspace'}}
|
||||
select.js-trello-parent-workspace(id='trello-parent-workspace')
|
||||
option(value="") {{_ 'trello-parent-workspace-top'}}
|
||||
each flatWorkspaceNodes
|
||||
option(value=id)= label
|
||||
p.trello-select-controls
|
||||
button.primary.js-trello-select-all(type="button") {{_ 'select-all'}}
|
||||
button.primary.js-trello-unselect-all(type="button") {{_ 'unselect-all'}}
|
||||
.trello-workspaces
|
||||
each workspaceList
|
||||
.trello-workspace
|
||||
a.flex.trello-workspace-title.js-toggle-workspace
|
||||
.materialCheckBox(class="{{#if workspaceSelected}}is-checked{{/if}}")
|
||||
b {{name}}
|
||||
.trello-boards
|
||||
each boards
|
||||
a.flex.trello-board.js-toggle-board(data-board-id="{{id}}")
|
||||
.materialCheckBox(class="{{#if boardSelected}}is-checked{{/if}}")
|
||||
span {{name}}
|
||||
if closed
|
||||
span.quiet ({{_ 'archived'}})
|
||||
if canStartImport
|
||||
p
|
||||
button.primary.wide.js-trello-import-selected(type="button") {{_ 'trello-import-selected'}}
|
||||
if currentJob
|
||||
hr
|
||||
.trello-import-job
|
||||
h3 {{_ 'trello-import-progress'}}
|
||||
with currentJob
|
||||
p.trello-progress-label
|
||||
b {{_ 'status'}}:
|
||||
span {{_ status}} ({{jobProgressText}})
|
||||
.trello-progress-bar
|
||||
.trello-progress-fill(class="{{#if $eq status 'running'}}is-running{{/if}}" style="width: {{jobProgressPercent}}%")
|
||||
.trello-progress-percent {{jobProgressPercent}}%
|
||||
if lastError
|
||||
.warning {{lastError}}
|
||||
p.trello-job-controls
|
||||
if jobIsRunning
|
||||
button.primary.js-trello-cancel(type="button") {{_ 'trello-cancel'}}
|
||||
button.negate.js-trello-cancel-delete(type="button") {{_ 'trello-cancel-delete'}}
|
||||
if jobCanResume
|
||||
button.primary.js-trello-resume(type="button") {{_ 'trello-resume'}}
|
||||
button.negate.js-trello-cancel-delete(type="button") {{_ 'trello-cancel-delete'}}
|
||||
if jobIsFinished
|
||||
button.negate.js-trello-cancel-delete(type="button") {{_ 'trello-delete-imported'}}
|
||||
button.primary.js-trello-clear(type="button") {{_ 'trello-clear-job'}}
|
||||
|
||||
if jobResults.length
|
||||
.trello-import-results
|
||||
each jobResults
|
||||
p.trello-import-result(class="{{#if success}}is-ok{{else}}is-error{{/if}}")
|
||||
if success
|
||||
i.fa.fa-check
|
||||
span {{title}} — {{attachmentsImported}} {{_ 'attachments'}}
|
||||
else
|
||||
i.fa.fa-warning
|
||||
span {{trelloBoardId}}: {{this.error}}
|
||||
|
||||
if jobHasErrors
|
||||
.trello-import-errors
|
||||
p
|
||||
b {{_ 'trello-import-errors'}}
|
||||
button.primary.js-trello-copy-errors(type="button") {{_ 'copy-to-clipboard'}}
|
||||
if copied.get
|
||||
span.quiet {{_ 'copied'}}
|
||||
textarea.js-trello-errors-text(readonly rows="8" wrap="off")= jobErrorText
|
||||
|
||||
template(name="importMapMembers")
|
||||
h2 {{_ 'import-map-members'}}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { csvGetMembersToMap } from './csvMembersMapper';
|
||||
import getSlug from 'limax';
|
||||
import { UserSearchIndex } from '/models/users';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import TrelloImportJobs from '/models/trelloImportJobs';
|
||||
|
||||
const Papa = require('papaparse');
|
||||
|
||||
@@ -44,6 +46,92 @@ function _prepareAdditionalData(dataObject) {
|
||||
return membersToMap;
|
||||
}
|
||||
|
||||
// All Trello file/text imports are run on the server over HTTP instead of the
|
||||
// DDP `importBoard` method. This matters for correctness, not just size: Meteor
|
||||
// automatically re-sends an unacknowledged method call on every reconnect, so if
|
||||
// an import is heavy (or the connection hiccups) the WebSocket can enter an
|
||||
// endless drop/retry loop (the "Invalid frame header" flicker). An HTTP request
|
||||
// is never auto-retried, and a .zip's attachment bytes never touch the realtime
|
||||
// connection at all. The body is either a .zip File (application/zip) or a JSON
|
||||
// string { board, membersMapping } (application/json).
|
||||
async function postTrelloImport(body, contentType) {
|
||||
const token =
|
||||
(window.localStorage && window.localStorage.getItem('Meteor.loginToken')) || '';
|
||||
const resp = await fetch('/import-trello', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body,
|
||||
});
|
||||
let result = {};
|
||||
try {
|
||||
result = await resp.json();
|
||||
} catch (e) {
|
||||
result = {};
|
||||
}
|
||||
if (!resp.ok || result.error) {
|
||||
throw new Error(result.error || 'import-trello-failed');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// A safe URL slug for an imported board. Trello exports name the board `name`,
|
||||
// WeKan exports use `title`; getSlug (limax) throws on undefined, so guard it.
|
||||
function boardSlug(data) {
|
||||
const raw = (data && (data.title || data.name)) || '';
|
||||
return (raw && getSlug(raw)) || 'imported-board';
|
||||
}
|
||||
|
||||
// Navigate to a freshly server-imported board. The HTTP import (unlike a DDP
|
||||
// method) does not push the new board's documents to this client, so we first
|
||||
// subscribe to the board and wait until its lists/swimlanes/cards are loaded
|
||||
// into Minimongo — otherwise the board opens with an empty Swimlanes view until
|
||||
// the page is reloaded. The subscription is left running so the data stays
|
||||
// available when the board is reopened from All Boards in the same session. A
|
||||
// timeout is the safety net in case the subscription never signals ready.
|
||||
function goToImportedBoard(boardId, slug) {
|
||||
let navigated = false;
|
||||
const go = () => {
|
||||
if (navigated) return;
|
||||
navigated = true;
|
||||
FlowRouter.go('board', { id: boardId, slug });
|
||||
};
|
||||
Meteor.subscribe('board', boardId, false, { onReady: go });
|
||||
Meteor.setTimeout(go, 5000);
|
||||
}
|
||||
|
||||
// Find a workspace node by name anywhere in the user's personal workspace tree.
|
||||
function findWorkspaceByName(nodes, name) {
|
||||
for (const node of nodes || []) {
|
||||
if (node.name === name) return node;
|
||||
if (node.children) {
|
||||
const found = findWorkspaceByName(node.children, name);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assign an imported board to a personal workspace named `wsName`, creating the
|
||||
// workspace (under an optional parent) only if one with that name doesn't exist.
|
||||
function assignBoardToNamedWorkspace(boardId, wsName, parentId = null) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const tree = (user && user.profile && user.profile.boardWorkspacesTree) || [];
|
||||
const existing = findWorkspaceByName(tree, wsName);
|
||||
if (existing) {
|
||||
Meteor.call('assignBoardToWorkspace', boardId, existing.id);
|
||||
return;
|
||||
}
|
||||
Meteor.call('createWorkspace', { parentId, name: wsName }, (err, node) => {
|
||||
if (!err && node) {
|
||||
Meteor.call('assignBoardToWorkspace', boardId, node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Template.import.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.steps = ['importTextarea', 'importMapMembers'];
|
||||
@@ -51,6 +139,8 @@ Template.import.onCreated(function () {
|
||||
this.importedData = new ReactiveVar();
|
||||
this.membersToMap = new ReactiveVar([]);
|
||||
this.importSource = Session.get('importSource');
|
||||
// True while a Trello .zip package is being uploaded/imported server-side.
|
||||
this.zipImporting = new ReactiveVar(false);
|
||||
|
||||
this.nextStep = () => {
|
||||
const nextStepIndex = this._currentStepIndex.get() + 1;
|
||||
@@ -65,10 +155,10 @@ Template.import.onCreated(function () {
|
||||
this.error.set(error);
|
||||
};
|
||||
|
||||
this.importData = (evt, dataSource) => {
|
||||
this.importData = async (evt, dataSource) => {
|
||||
evt.preventDefault();
|
||||
const input = this.find('.js-import-json').value;
|
||||
if (dataSource === 'csv') {
|
||||
const input = this.find('.js-import-json').value;
|
||||
const csv = input.indexOf('\t') > 0 ? input.replace(/(\t)/g, ',') : input;
|
||||
const ret = Papa.parse(csv);
|
||||
if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
|
||||
@@ -76,52 +166,136 @@ Template.import.onCreated(function () {
|
||||
const membersToMap = _prepareAdditionalData(ret.data);
|
||||
this.membersToMap.set(membersToMap);
|
||||
this.nextStep();
|
||||
} else {
|
||||
try {
|
||||
const dataObject = JSON.parse(input);
|
||||
this.setError('');
|
||||
this.importedData.set(dataObject);
|
||||
const membersToMap = _prepareAdditionalData(dataObject);
|
||||
// store members data and mapping in Session
|
||||
// (we go deep and 2-way, so storing in data context is not a viable option)
|
||||
this.membersToMap.set(membersToMap);
|
||||
this.nextStep();
|
||||
} catch (e) {
|
||||
this.setError('error-json-malformed');
|
||||
return;
|
||||
}
|
||||
// Trello: a .zip package (one or more board .json files plus per-board
|
||||
// attachment subdirectories from the Trello Attachments Downloader) imports
|
||||
// all of its boards at once, through a separate field from the single .json.
|
||||
if (dataSource === 'trello') {
|
||||
const zipEl = this.find('.js-import-zip-file');
|
||||
if (zipEl && zipEl.files && zipEl.files[0]) {
|
||||
await this.importTrelloZip(zipEl.files[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
// A single board: JSON may come from an uploaded .json file (large Trello
|
||||
// exports are awkward to paste) or from the textarea.
|
||||
let input = this.find('.js-import-json').value;
|
||||
const jsonFileEl = this.find('.js-import-json-file');
|
||||
if (jsonFileEl && jsonFileEl.files && jsonFileEl.files[0]) {
|
||||
input = await jsonFileEl.files[0].text();
|
||||
}
|
||||
const dataObject = JSON.parse(input);
|
||||
this.setError('');
|
||||
|
||||
// Trello: remember the target personal-workspace name for finishImport.
|
||||
this.workspaceName = '';
|
||||
if (dataSource === 'trello') {
|
||||
const wsEl = this.find('.js-import-workspace-name');
|
||||
this.workspaceName = wsEl && wsEl.value ? wsEl.value.trim() : '';
|
||||
}
|
||||
|
||||
this.importedData.set(dataObject);
|
||||
const membersToMap = _prepareAdditionalData(dataObject);
|
||||
// store members data and mapping in Session
|
||||
// (we go deep and 2-way, so storing in data context is not a viable option)
|
||||
this.membersToMap.set(membersToMap);
|
||||
this.nextStep();
|
||||
} catch (e) {
|
||||
this.setError('error-json-malformed');
|
||||
}
|
||||
};
|
||||
|
||||
this.finishImport = () => {
|
||||
const additionalData = {};
|
||||
// Upload a Trello .zip package to the server, which extracts it (with
|
||||
// zip-bomb / path-traversal guards), imports every board and streams each
|
||||
// attachment to the Default storage, then go to All Boards.
|
||||
this.importTrelloZip = async (zipFile) => {
|
||||
this.setError('');
|
||||
const wsEl = this.find('.js-import-workspace-name');
|
||||
const workspaceName = wsEl && wsEl.value ? wsEl.value.trim() : '';
|
||||
|
||||
this.zipImporting.set(true);
|
||||
let result;
|
||||
try {
|
||||
result = await postTrelloImport(zipFile, 'application/zip');
|
||||
} catch (e) {
|
||||
this.zipImporting.set(false);
|
||||
this.setError((e && e.message) || 'import-trello-failed');
|
||||
return;
|
||||
}
|
||||
this.zipImporting.set(false);
|
||||
|
||||
(result.boardIds || []).forEach(boardId => {
|
||||
if (workspaceName) {
|
||||
assignBoardToNamedWorkspace(boardId, workspaceName);
|
||||
}
|
||||
});
|
||||
Session.set('fromBoard', null);
|
||||
// Go to All Boards, where the newly imported boards appear.
|
||||
FlowRouter.go('home');
|
||||
};
|
||||
|
||||
this.finishImport = async () => {
|
||||
const membersMapping = this.membersToMap.get();
|
||||
const mappingById = {};
|
||||
if (membersMapping) {
|
||||
const mappingById = {};
|
||||
membersMapping.forEach(member => {
|
||||
if (member.wekanId) {
|
||||
mappingById[member.id] = member.wekanId;
|
||||
}
|
||||
});
|
||||
additionalData.membersMapping = mappingById;
|
||||
}
|
||||
const importedData = this.importedData.get();
|
||||
|
||||
// Trello: import over HTTP (see postTrelloImport) so the realtime DDP
|
||||
// connection is never used for the board payload and can't enter the
|
||||
// drop/retry "Invalid frame header" flicker loop. Do NOT mutate the still-
|
||||
// mounted map-members template (e.g. clearing membersToMap) before
|
||||
// navigating away — that forces an empty re-render mid-teardown and can
|
||||
// throw "Can't select in removed DomRange". We navigate away, which
|
||||
// destroys the import templates.
|
||||
if (this.importSource === 'trello') {
|
||||
let result;
|
||||
try {
|
||||
result = await postTrelloImport(
|
||||
JSON.stringify({ board: importedData, membersMapping: mappingById }),
|
||||
'application/json',
|
||||
);
|
||||
} catch (e) {
|
||||
this.setError((e && e.message) || 'import-trello-failed');
|
||||
return;
|
||||
}
|
||||
const boardId = (result.boardIds || [])[0];
|
||||
if (!boardId) {
|
||||
this.setError('import-trello-failed');
|
||||
return;
|
||||
}
|
||||
Session.set('fromBoard', null);
|
||||
if (this.workspaceName) {
|
||||
assignBoardToNamedWorkspace(boardId, this.workspaceName);
|
||||
}
|
||||
goToImportedBoard(boardId, boardSlug(importedData));
|
||||
return;
|
||||
}
|
||||
|
||||
// wekan / csv: unchanged DDP import.
|
||||
this.membersToMap.set([]);
|
||||
Meteor.call(
|
||||
'importBoard',
|
||||
this.importedData.get(),
|
||||
additionalData,
|
||||
importedData,
|
||||
{ membersMapping: mappingById },
|
||||
this.importSource,
|
||||
Session.get('fromBoard'),
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
this.setError(err.error);
|
||||
} else {
|
||||
let title = getSlug(this.importedData.get().title) || 'imported-board';
|
||||
Session.set('fromBoard', null);
|
||||
FlowRouter.go('board', {
|
||||
id: res,
|
||||
slug: title,
|
||||
slug: boardSlug(importedData),
|
||||
});
|
||||
//Utils.goBoardId(res);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -135,6 +309,9 @@ Template.import.helpers({
|
||||
currentTemplate() {
|
||||
return Template.instance().steps[Template.instance()._currentStepIndex.get()];
|
||||
},
|
||||
zipImporting() {
|
||||
return Template.instance().zipImporting.get();
|
||||
},
|
||||
});
|
||||
|
||||
Template.importTextarea.helpers({
|
||||
@@ -149,6 +326,9 @@ Template.importTextarea.helpers({
|
||||
return 'import-json-placeholder';
|
||||
}
|
||||
},
|
||||
isTrelloImport() {
|
||||
return Session.get('importSource') === 'trello';
|
||||
},
|
||||
});
|
||||
|
||||
Template.importTextarea.events({
|
||||
@@ -320,7 +500,11 @@ Template.importMapMembersAddPopup.onCreated(function () {
|
||||
});
|
||||
|
||||
Template.importMapMembersAddPopup.onRendered(function () {
|
||||
this.find('.js-search-member-input').focus();
|
||||
// Guard against the DOM range being gone (e.g. the popup was closed during a
|
||||
// re-render) — calling find/$ then throws "Can't select in removed DomRange".
|
||||
if (this.view && this.view.isDestroyed) return;
|
||||
const input = this.find('.js-search-member-input');
|
||||
if (input) input.focus();
|
||||
});
|
||||
|
||||
Template.importMapMembersAddPopup.onDestroyed(function () {
|
||||
@@ -380,3 +564,310 @@ Template.importMapMembersAddPopup.helpers({
|
||||
return importMemberPopupState.noResults;
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Live Trello API import: key/token -> list workspaces & boards -> import
|
||||
// selected boards (with attachments) server-side, placing each under a
|
||||
// personal workspace named after its Trello workspace.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function flattenWorkspaceTree(nodes, depth = 0, acc = []) {
|
||||
(nodes || []).forEach(node => {
|
||||
acc.push({ id: node.id, label: `${'— '.repeat(depth)}${node.name}` });
|
||||
if (node.children && node.children.length) {
|
||||
flattenWorkspaceTree(node.children, depth + 1, acc);
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Build the copy-paste-friendly error text for a job: the error log plus a
|
||||
// summary line per failed board.
|
||||
function jobErrorText(job) {
|
||||
if (!job) return '';
|
||||
const lines = [];
|
||||
(job.errorLog || []).forEach(line => lines.push(line));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
Template.importTrelloApi.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
this.workspaces = new ReactiveVar([]);
|
||||
this.copied = new ReactiveVar(false);
|
||||
// Board selection state: map of trelloBoardId -> true. Tracked here (rather
|
||||
// than via DOM checkboxes) because the UI uses animated .materialCheckBox
|
||||
// elements, not native inputs.
|
||||
this.selectedBoards = new ReactiveVar({});
|
||||
// The import itself runs server-side as a persisted job; watch it reactively
|
||||
// so progress survives navigating away and back.
|
||||
this.subscribe('trelloImportJobs');
|
||||
});
|
||||
|
||||
// Collect every board id across all listed workspaces.
|
||||
function allBoardIds(workspaces) {
|
||||
const ids = {};
|
||||
(workspaces || []).forEach(ws => {
|
||||
(ws.boards || []).forEach(b => {
|
||||
ids[b.id] = true;
|
||||
});
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
Template.importTrelloApi.helpers({
|
||||
error() {
|
||||
return Template.instance().error;
|
||||
},
|
||||
loading() {
|
||||
return Template.instance().loading;
|
||||
},
|
||||
copied() {
|
||||
return Template.instance().copied;
|
||||
},
|
||||
hasWorkspaces() {
|
||||
return Template.instance().workspaces.get().length > 0;
|
||||
},
|
||||
workspaceList() {
|
||||
return Template.instance().workspaces.get();
|
||||
},
|
||||
boardSelected() {
|
||||
return !!Template.instance().selectedBoards.get()[this.id];
|
||||
},
|
||||
workspaceSelected() {
|
||||
const sel = Template.instance().selectedBoards.get();
|
||||
const boards = this.boards || [];
|
||||
return boards.length > 0 && boards.every(b => sel[b.id]);
|
||||
},
|
||||
flatWorkspaceNodes() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const tree = (user && user.profile && user.profile.boardWorkspacesTree) || [];
|
||||
return flattenWorkspaceTree(tree);
|
||||
},
|
||||
credsSaved() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return !!(user && user.profile && user.profile.trelloApiSaved);
|
||||
},
|
||||
|
||||
// --- current background job ---
|
||||
currentJob() {
|
||||
return TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
},
|
||||
jobIsRunning() {
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
return job && job.status === 'running';
|
||||
},
|
||||
jobCanResume() {
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
return job && (job.status === 'paused' || job.status === 'error');
|
||||
},
|
||||
jobIsFinished() {
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
return job && (job.status === 'done' || job.status === 'cancelled');
|
||||
},
|
||||
canStartImport() {
|
||||
// Don't start a second import while one is active (running/paused/error),
|
||||
// which would create a hidden concurrent job.
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
return !job || job.status === 'done' || job.status === 'cancelled';
|
||||
},
|
||||
jobProgressText() {
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
if (!job) return '';
|
||||
return `${job.currentIndex} / ${job.total}`;
|
||||
},
|
||||
jobProgressPercent() {
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
if (!job || !job.total) return 0;
|
||||
return Math.round((job.currentIndex / job.total) * 100);
|
||||
},
|
||||
jobResults() {
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
return (job && job.results) || [];
|
||||
},
|
||||
jobHasErrors() {
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
return !!(job && job.errorLog && job.errorLog.length);
|
||||
},
|
||||
jobErrorText() {
|
||||
return jobErrorText(TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } }));
|
||||
},
|
||||
});
|
||||
|
||||
Template.importTrelloApi.events({
|
||||
'click .js-trello-save-creds'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const key = tpl.find('.js-trello-key').value.trim();
|
||||
const token = tpl.find('.js-trello-token').value.trim();
|
||||
if (!key || !token) {
|
||||
tpl.error.set('trello-api-credentials-required');
|
||||
return;
|
||||
}
|
||||
tpl.error.set('');
|
||||
Meteor.call('saveTrelloCredentials', key, token, err => {
|
||||
if (err) {
|
||||
tpl.error.set(err.reason || err.error || 'trello-api-error');
|
||||
return;
|
||||
}
|
||||
// Don't keep the token sitting in the browser; it now lives server-side.
|
||||
tpl.find('.js-trello-key').value = '';
|
||||
tpl.find('.js-trello-token').value = '';
|
||||
});
|
||||
},
|
||||
'click .js-trello-delete-creds'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.error.set('');
|
||||
Meteor.call('deleteTrelloCredentials', err => {
|
||||
if (err) tpl.error.set(err.reason || err.error || 'trello-api-error');
|
||||
});
|
||||
tpl.find('.js-trello-key').value = '';
|
||||
tpl.find('.js-trello-token').value = '';
|
||||
},
|
||||
'click .js-trello-list-workspaces'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
// key/token may be empty when saved credentials exist; the server falls
|
||||
// back to the saved ones and returns an error if neither is available.
|
||||
const key = tpl.find('.js-trello-key').value.trim();
|
||||
const token = tpl.find('.js-trello-token').value.trim();
|
||||
tpl.error.set('');
|
||||
tpl.loading.set(true);
|
||||
Meteor.call('trelloListWorkspaces', key, token, (err, res) => {
|
||||
tpl.loading.set(false);
|
||||
if (err) {
|
||||
tpl.error.set(err.reason || err.error || 'trello-api-error');
|
||||
tpl.workspaces.set([]);
|
||||
tpl.selectedBoards.set({});
|
||||
} else {
|
||||
const workspaces = res || [];
|
||||
tpl.workspaces.set(workspaces);
|
||||
// Preselect all boards by default.
|
||||
tpl.selectedBoards.set(allBoardIds(workspaces));
|
||||
}
|
||||
});
|
||||
},
|
||||
// Toggle a single board's animated checkbox.
|
||||
'click .js-toggle-board'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const id = this.id;
|
||||
const sel = { ...tpl.selectedBoards.get() };
|
||||
if (sel[id]) {
|
||||
delete sel[id];
|
||||
} else {
|
||||
sel[id] = true;
|
||||
}
|
||||
tpl.selectedBoards.set(sel);
|
||||
},
|
||||
// Toggle all boards in a workspace: if all are selected, clear them; else
|
||||
// select them all.
|
||||
'click .js-toggle-workspace'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const boards = this.boards || [];
|
||||
const sel = { ...tpl.selectedBoards.get() };
|
||||
const allSelected = boards.length > 0 && boards.every(b => sel[b.id]);
|
||||
boards.forEach(b => {
|
||||
if (allSelected) {
|
||||
delete sel[b.id];
|
||||
} else {
|
||||
sel[b.id] = true;
|
||||
}
|
||||
});
|
||||
tpl.selectedBoards.set(sel);
|
||||
},
|
||||
'click .js-trello-select-all'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.selectedBoards.set(allBoardIds(tpl.workspaces.get()));
|
||||
},
|
||||
'click .js-trello-unselect-all'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.selectedBoards.set({});
|
||||
},
|
||||
'click .js-trello-import-selected'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const key = tpl.find('.js-trello-key').value.trim();
|
||||
const token = tpl.find('.js-trello-token').value.trim();
|
||||
const sel = tpl.selectedBoards.get();
|
||||
const boardIds = Object.keys(sel).filter(id => sel[id]);
|
||||
if (!boardIds.length) {
|
||||
tpl.error.set('trello-select-boards');
|
||||
return;
|
||||
}
|
||||
const parentEl = tpl.find('.js-trello-parent-workspace');
|
||||
const parentId = parentEl && parentEl.value ? parentEl.value : null;
|
||||
|
||||
tpl.error.set('');
|
||||
// Start the server-side job; progress shows up via the subscription. The
|
||||
// user is free to navigate away and come back.
|
||||
Meteor.call('trelloStartImport', key, token, boardIds, parentId, err => {
|
||||
if (err) {
|
||||
tpl.error.set(err.reason || err.error || 'trello-api-error');
|
||||
}
|
||||
});
|
||||
},
|
||||
'click .js-trello-resume'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
if (!job) return;
|
||||
const key = tpl.find('.js-trello-key').value.trim();
|
||||
const token = tpl.find('.js-trello-token').value.trim();
|
||||
if (!key || !token) {
|
||||
tpl.error.set('trello-api-credentials-required');
|
||||
return;
|
||||
}
|
||||
tpl.error.set('');
|
||||
Meteor.call('trelloResumeImport', job._id, key, token, err => {
|
||||
if (err) tpl.error.set(err.reason || err.error || 'trello-api-error');
|
||||
});
|
||||
},
|
||||
'click .js-trello-cancel'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
if (!job) return;
|
||||
Meteor.call('trelloCancelImport', job._id, false);
|
||||
},
|
||||
'click .js-trello-cancel-delete'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
if (!job) return;
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(TAPi18n.__('trello-cancel-delete-confirm'))) return;
|
||||
Meteor.call('trelloCancelImport', job._id, true);
|
||||
},
|
||||
'click .js-trello-clear'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
if (!job) return;
|
||||
Meteor.call('trelloClearImportJob', job._id, false);
|
||||
},
|
||||
'click .js-trello-copy-errors'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const job = TrelloImportJobs.findOne({}, { sort: { createdAt: -1 } });
|
||||
const text = jobErrorText(job);
|
||||
if (!text) return;
|
||||
const done = () => {
|
||||
tpl.copied.set(true);
|
||||
setTimeout(() => tpl.copied.set(false), 2000);
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(done, () => {
|
||||
// Fall back to selecting the textarea so the user can copy manually.
|
||||
const ta = tpl.find('.js-trello-errors-text');
|
||||
if (ta) {
|
||||
ta.focus();
|
||||
ta.select();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const ta = tpl.find('.js-trello-errors-text');
|
||||
if (ta) {
|
||||
ta.focus();
|
||||
ta.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
done();
|
||||
} catch (e) {
|
||||
// user can copy manually from the selected textarea
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -4,15 +4,24 @@ export function wekanGetMembersToMap(data) {
|
||||
// we will work on the list itself (an ordered array of objects) when a
|
||||
// mapping is done, we add a 'wekan' field to the object representing the
|
||||
// imported member
|
||||
const membersToMap = data.members;
|
||||
const users = data.users;
|
||||
const membersToMap = data.members || [];
|
||||
const users = data.users || [];
|
||||
// auto-map based on username
|
||||
const mappable = [];
|
||||
membersToMap.forEach(importedMember => {
|
||||
importedMember.id = importedMember.userId;
|
||||
delete importedMember.userId;
|
||||
const user = users.filter(user => {
|
||||
return user._id === importedMember.id;
|
||||
})[0];
|
||||
// The exported `users` array only contains users that still exist in the
|
||||
// source instance, so a member whose account was deleted (or any other
|
||||
// dangling user reference, e.g. from an older export) has no matching
|
||||
// user. Skip it instead of dereferencing `undefined`, which would throw
|
||||
// and make the whole import fail as "error-json-malformed".
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
if (user.profile && user.profile.fullname) {
|
||||
importedMember.fullName = user.profile.fullname;
|
||||
}
|
||||
@@ -21,6 +30,7 @@ export function wekanGetMembersToMap(data) {
|
||||
if (wekanUser) {
|
||||
importedMember.wekanId = wekanUser._id;
|
||||
}
|
||||
mappable.push(importedMember);
|
||||
});
|
||||
return membersToMap;
|
||||
return mappable;
|
||||
}
|
||||
@@ -194,6 +194,19 @@ Template.listBody.onCreated(function () {
|
||||
const clickedLinkedReference = $target.closest('.js-linked-link').length > 0;
|
||||
const clickedChecklist = $target.closest('.minicard-checklist').length > 0;
|
||||
|
||||
// When multi-selection is active (or Shift is held), a click anywhere on the
|
||||
// minicard toggles its selection. This must be checked before the title and
|
||||
// checklist handlers below, otherwise clicking a card's title (which covers
|
||||
// most of the minicard) would open the card details instead of (de)selecting
|
||||
// it, making it impossible to check some cards in multi-selection mode.
|
||||
if (MultiSelection.isActive() || evt.shiftKey) {
|
||||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
|
||||
MultiSelection[methodName](card._id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickedChecklist) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
@@ -214,16 +227,7 @@ Template.listBody.onCreated(function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (MultiSelection.isActive() || evt.shiftKey) {
|
||||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
|
||||
MultiSelection[methodName](card._id);
|
||||
|
||||
// If the card is already selected, we want to de-select it.
|
||||
// XXX We should probably modify the minicard href attribute instead of
|
||||
// overwriting the event in case the card is already selected.
|
||||
} else if (Utils.isMiniScreen()) {
|
||||
if (Utils.isMiniScreen()) {
|
||||
evt.preventDefault();
|
||||
Session.set('popupCardId', card._id);
|
||||
this.cardDetailsPopup(evt);
|
||||
@@ -244,9 +248,18 @@ Template.listBody.onCreated(function () {
|
||||
};
|
||||
|
||||
this.toggleMultiSelection = (evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
MultiSelection.toggle(Template.currentData()._id);
|
||||
// Resolve the card from the clicked checkbox element. Using
|
||||
// Blaze.getData(evt.currentTarget) is reliable for the per-card data
|
||||
// context, whereas Template.currentData() can resolve to the enclosing
|
||||
// list context and toggle the wrong id (so the checkbox appears to do
|
||||
// nothing). Falls back to Template.currentData() just in case.
|
||||
const card =
|
||||
Blaze.getData(evt.currentTarget) || Template.currentData();
|
||||
if (card && card._id) {
|
||||
MultiSelection.toggle(card._id);
|
||||
}
|
||||
};
|
||||
|
||||
this.cardDetailsPopup = (event) => {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/* Search + pagination controls for the admin report tabs (files, rules,
|
||||
boards, cards). Styled to match the admin People panel (peopleBody.css). */
|
||||
|
||||
.admin-report-search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 34px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-report-search input {
|
||||
flex: 1;
|
||||
max-width: 320px;
|
||||
padding: 0 8px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.admin-report-search button {
|
||||
min-width: 90px;
|
||||
height: 34px;
|
||||
background: #01628c;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0.4vw;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.admin-report-search button:hover {
|
||||
background: #005377;
|
||||
}
|
||||
|
||||
.admin-report-search button:active {
|
||||
background: #004766;
|
||||
}
|
||||
|
||||
.admin-report-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-report-pagination button {
|
||||
min-width: 32px;
|
||||
height: 30px;
|
||||
border: 1px solid #bbb;
|
||||
background: #fff;
|
||||
border-radius: 0.4vw;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-report-pagination button.active {
|
||||
background: #01628c;
|
||||
border-color: #01628c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-report-pagination button.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.admin-report-page-info {
|
||||
min-width: 72px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
@@ -8,27 +8,27 @@ template(name="adminReports")
|
||||
ul
|
||||
li
|
||||
a.js-report-broken(data-id="report-broken")
|
||||
span.emoji-icon 🔗
|
||||
i.fa.fa-chain-broken
|
||||
| {{_ 'broken-cards'}}
|
||||
|
||||
li
|
||||
a.js-report-files(data-id="report-files")
|
||||
span.emoji-icon 📎
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'filesReportTitle'}}
|
||||
|
||||
li
|
||||
a.js-report-rules(data-id="report-rules")
|
||||
span.emoji-icon ✨
|
||||
i.fa.fa-magic
|
||||
| {{_ 'rulesReportTitle'}}
|
||||
|
||||
li
|
||||
a.js-report-boards(data-id="report-boards")
|
||||
span.emoji-icon ✨
|
||||
i.fa.fa-columns
|
||||
| {{_ 'boardsReportTitle'}}
|
||||
|
||||
li
|
||||
a.js-report-cards(data-id="report-cards")
|
||||
span.emoji-icon ✨
|
||||
i.fa.fa-id-card-o
|
||||
| {{_ 'cardsReportTitle'}}
|
||||
|
||||
.main-body
|
||||
@@ -37,13 +37,13 @@ template(name="adminReports")
|
||||
else if showBrokenCardsReport.get
|
||||
+brokenCardsReport
|
||||
else if showFilesReport.get
|
||||
+filesReport
|
||||
+filesReport(currentPage=filesCurrentPage totalPages=filesTotalPages hasPrevPage=hasFilesPrevPage hasNextPage=hasFilesNextPage)
|
||||
else if showRulesReport.get
|
||||
+rulesReport
|
||||
+rulesReport(currentPage=rulesCurrentPage totalPages=rulesTotalPages hasPrevPage=hasRulesPrevPage hasNextPage=hasRulesNextPage)
|
||||
else if showBoardsReport.get
|
||||
+boardsReport
|
||||
+boardsReport(currentPage=boardsCurrentPage totalPages=boardsTotalPages hasPrevPage=hasBoardsPrevPage hasNextPage=hasBoardsNextPage)
|
||||
else if showCardsReport.get
|
||||
+cardsReport
|
||||
+cardsReport(currentPage=cardsCurrentPage totalPages=cardsTotalPages hasPrevPage=hasCardsPrevPage hasNextPage=hasCardsNextPage)
|
||||
|
||||
|
||||
template(name="brokenCardsReport")
|
||||
@@ -56,6 +56,17 @@ template(name="brokenCardsReport")
|
||||
|
||||
template(name="rulesReport")
|
||||
h1 {{_ 'rulesReportTitle'}}
|
||||
.admin-report-search
|
||||
input.js-rules-search-input(type="text" placeholder="{{_ 'search'}}")
|
||||
button.js-rules-search-button
|
||||
i.fa.fa-search
|
||||
| {{_ 'search'}}
|
||||
.admin-report-pagination
|
||||
button.js-rules-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
|
||||
| <
|
||||
span.admin-report-page-info {{currentPage}} / {{totalPages}}
|
||||
button.js-rules-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
|
||||
| >
|
||||
if resultsCount
|
||||
table
|
||||
thead
|
||||
@@ -77,6 +88,17 @@ template(name="rulesReport")
|
||||
|
||||
template(name="filesReport")
|
||||
h1 {{_ 'filesReportTitle'}}
|
||||
.admin-report-search
|
||||
input.js-files-search-input(type="text" placeholder="{{_ 'search'}}")
|
||||
button.js-files-search-button
|
||||
i.fa.fa-search
|
||||
| {{_ 'search'}}
|
||||
.admin-report-pagination
|
||||
button.js-files-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
|
||||
| <
|
||||
span.admin-report-page-info {{currentPage}} / {{totalPages}}
|
||||
button.js-files-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
|
||||
| >
|
||||
if resultsCount
|
||||
table
|
||||
thead
|
||||
@@ -102,6 +124,17 @@ template(name="filesReport")
|
||||
|
||||
template(name="cardsReport")
|
||||
h1 {{_ 'cardsReportTitle'}}
|
||||
.admin-report-search
|
||||
input.js-cards-search-input(type="text" placeholder="{{_ 'search'}}")
|
||||
button.js-cards-search-button
|
||||
i.fa.fa-search
|
||||
| {{_ 'search'}}
|
||||
.admin-report-pagination
|
||||
button.js-cards-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
|
||||
| <
|
||||
span.admin-report-page-info {{currentPage}} / {{totalPages}}
|
||||
button.js-cards-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
|
||||
| >
|
||||
if resultsCount
|
||||
table.table
|
||||
thead
|
||||
@@ -127,6 +160,17 @@ template(name="cardsReport")
|
||||
|
||||
template(name="boardsReport")
|
||||
h1 {{_ 'boardsReportTitle'}}
|
||||
.admin-report-search
|
||||
input.js-boards-search-input(type="text" placeholder="{{_ 'search'}}")
|
||||
button.js-boards-search-button
|
||||
i.fa.fa-search
|
||||
| {{_ 'search'}}
|
||||
.admin-report-pagination
|
||||
button.js-boards-prev-page(class="{{#if hasPrevPage}}{{else}}disabled{{/if}}")
|
||||
| <
|
||||
span.admin-report-page-info {{currentPage}} / {{totalPages}}
|
||||
button.js-boards-next-page(class="{{#if hasNextPage}}{{else}}disabled{{/if}}")
|
||||
| >
|
||||
if resultsCount
|
||||
table.table
|
||||
thead
|
||||
|
||||
@@ -3,8 +3,6 @@ import { TAPi18n } from '/imports/i18n';
|
||||
import Attachments, { AttachmentStorage } from '/models/attachments';
|
||||
import { CardSearchPaged } from '/client/lib/cardSearch';
|
||||
import SessionData from '/models/usersessiondata';
|
||||
import { QueryParams } from '/config/query-classes';
|
||||
import { OPERATOR_LIMIT } from '/config/search-const';
|
||||
import Boards from '/models/boards';
|
||||
import Cards from '/models/cards';
|
||||
import Rules from '/models/rules';
|
||||
@@ -35,8 +33,12 @@ function abbreviate(text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
function collectionResults(collection) {
|
||||
return collection.find();
|
||||
// The report publications already send only the current page (server-side
|
||||
// search + limit/skip, sorted). Display exactly what was published, applying
|
||||
// the same sort so minimongo order matches the server page. Re-applying
|
||||
// skip/limit here would paginate an already-paginated set.
|
||||
function collectionResults(collection, sort) {
|
||||
return collection.find({}, sort ? { sort } : {});
|
||||
}
|
||||
|
||||
function collectionResultsCount(collection) {
|
||||
@@ -45,6 +47,22 @@ function collectionResultsCount(collection) {
|
||||
|
||||
// --- adminReports template ---
|
||||
|
||||
// Rows per page for the paginated reports (files, rules, boards, cards).
|
||||
// Kept in sync with the admin People panel so all admin lists page alike.
|
||||
const REPORTS_PER_PAGE = 25;
|
||||
|
||||
// Static description of each paginated report: which page/count reactive vars
|
||||
// it uses, which publication feeds it and which count method backs it. Lets a
|
||||
// single loadReport() drive all of them the same way.
|
||||
function reportConfig(tmpl) {
|
||||
return {
|
||||
'report-files': { page: tmpl.filesPage, count: tmpl.filesCount, search: tmpl.filesSearch, pub: 'attachmentsList', countMethod: 'getAttachmentsReportCount' },
|
||||
'report-rules': { page: tmpl.rulesPage, count: tmpl.rulesCount, search: tmpl.rulesSearch, pub: 'rulesReport', countMethod: 'getRulesReportCount' },
|
||||
'report-boards': { page: tmpl.boardsPage, count: tmpl.boardsCount, search: tmpl.boardsSearch, pub: 'boardsReport', countMethod: 'getBoardsReportCount' },
|
||||
'report-cards': { page: tmpl.cardsPage, count: tmpl.cardsCount, search: tmpl.cardsSearch, pub: 'cardsReport', countMethod: 'getCardsReportCount' },
|
||||
};
|
||||
}
|
||||
|
||||
Template.adminReports.onCreated(function () {
|
||||
this.subscription = null;
|
||||
this.showFilesReport = new ReactiveVar(false);
|
||||
@@ -56,6 +74,60 @@ Template.adminReports.onCreated(function () {
|
||||
this.sessionId = SessionData.getSessionId();
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
|
||||
// Pagination state, one current-page + total-count pair per report.
|
||||
this.activeReport = new ReactiveVar('');
|
||||
this.filesPage = new ReactiveVar(1);
|
||||
this.rulesPage = new ReactiveVar(1);
|
||||
this.boardsPage = new ReactiveVar(1);
|
||||
this.cardsPage = new ReactiveVar(1);
|
||||
this.filesCount = new ReactiveVar(0);
|
||||
this.rulesCount = new ReactiveVar(0);
|
||||
this.boardsCount = new ReactiveVar(0);
|
||||
this.cardsCount = new ReactiveVar(0);
|
||||
// Current search term per report (empty string = no filter).
|
||||
this.filesSearch = new ReactiveVar('');
|
||||
this.rulesSearch = new ReactiveVar('');
|
||||
this.boardsSearch = new ReactiveVar('');
|
||||
this.cardsSearch = new ReactiveVar('');
|
||||
|
||||
// (Re)subscribe the given report for its current page and refresh its total
|
||||
// count. Server-side search + limit/skip means only the matching page ever
|
||||
// reaches minimongo.
|
||||
this.loadReport = (reportId) => {
|
||||
const cfg = reportConfig(this)[reportId];
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
// Note: we deliberately do not toggle the global `loading` spinner here.
|
||||
// The spinner is shown once on tab switch (switchMenu); paging/searching
|
||||
// re-subscribes silently so the search box keeps focus and the report
|
||||
// stays mounted, just like the admin People panel.
|
||||
if (this.subscription) {
|
||||
this.subscription.stop();
|
||||
}
|
||||
const searchTerm = cfg.search.get();
|
||||
const limit = REPORTS_PER_PAGE;
|
||||
const skip = (cfg.page.get() - 1) * REPORTS_PER_PAGE;
|
||||
this.subscription = Meteor.subscribe(cfg.pub, searchTerm, limit, skip, () => {
|
||||
this.loading.set(false);
|
||||
});
|
||||
Meteor.call(cfg.countMethod, searchTerm, (error, count) => {
|
||||
if (error) {
|
||||
console.error(`Failed to load ${cfg.countMethod}:`, error);
|
||||
return;
|
||||
}
|
||||
const total = count || 0;
|
||||
const totalPages = Math.max(1, Math.ceil(total / REPORTS_PER_PAGE));
|
||||
// If rows were removed while on a now-out-of-range page, clamp and reload.
|
||||
if (cfg.page.get() > totalPages) {
|
||||
cfg.page.set(totalPages);
|
||||
this.loadReport(reportId);
|
||||
return;
|
||||
}
|
||||
cfg.count.set(total);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
Template.adminReports.helpers({
|
||||
@@ -80,6 +152,39 @@ Template.adminReports.helpers({
|
||||
loading() {
|
||||
return Template.instance().loading;
|
||||
},
|
||||
|
||||
// --- Pagination helpers, passed down into each report sub-template ---
|
||||
filesCurrentPage() { return Template.instance().filesPage.get(); },
|
||||
filesTotalPages() { return Math.max(1, Math.ceil((Template.instance().filesCount.get() || 0) / REPORTS_PER_PAGE)); },
|
||||
hasFilesPrevPage() { return Template.instance().filesPage.get() > 1; },
|
||||
hasFilesNextPage() {
|
||||
const tpl = Template.instance();
|
||||
return tpl.filesPage.get() < Math.max(1, Math.ceil((tpl.filesCount.get() || 0) / REPORTS_PER_PAGE));
|
||||
},
|
||||
|
||||
rulesCurrentPage() { return Template.instance().rulesPage.get(); },
|
||||
rulesTotalPages() { return Math.max(1, Math.ceil((Template.instance().rulesCount.get() || 0) / REPORTS_PER_PAGE)); },
|
||||
hasRulesPrevPage() { return Template.instance().rulesPage.get() > 1; },
|
||||
hasRulesNextPage() {
|
||||
const tpl = Template.instance();
|
||||
return tpl.rulesPage.get() < Math.max(1, Math.ceil((tpl.rulesCount.get() || 0) / REPORTS_PER_PAGE));
|
||||
},
|
||||
|
||||
boardsCurrentPage() { return Template.instance().boardsPage.get(); },
|
||||
boardsTotalPages() { return Math.max(1, Math.ceil((Template.instance().boardsCount.get() || 0) / REPORTS_PER_PAGE)); },
|
||||
hasBoardsPrevPage() { return Template.instance().boardsPage.get() > 1; },
|
||||
hasBoardsNextPage() {
|
||||
const tpl = Template.instance();
|
||||
return tpl.boardsPage.get() < Math.max(1, Math.ceil((tpl.boardsCount.get() || 0) / REPORTS_PER_PAGE));
|
||||
},
|
||||
|
||||
cardsCurrentPage() { return Template.instance().cardsPage.get(); },
|
||||
cardsTotalPages() { return Math.max(1, Math.ceil((Template.instance().cardsCount.get() || 0) / REPORTS_PER_PAGE)); },
|
||||
hasCardsPrevPage() { return Template.instance().cardsPage.get() > 1; },
|
||||
hasCardsNextPage() {
|
||||
const tpl = Template.instance();
|
||||
return tpl.cardsPage.get() < Math.max(1, Math.ceil((tpl.cardsCount.get() || 0) / REPORTS_PER_PAGE));
|
||||
},
|
||||
});
|
||||
|
||||
Template.adminReports.events({
|
||||
@@ -98,8 +203,60 @@ Template.adminReports.events({
|
||||
'click a.js-report-boards'(event) {
|
||||
switchMenu(event, Template.instance());
|
||||
},
|
||||
|
||||
// --- Pagination controls (buttons live inside the report sub-templates,
|
||||
// their clicks bubble up to this parent template) ---
|
||||
'click .js-files-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-files'); },
|
||||
'click .js-files-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-files'); },
|
||||
'click .js-rules-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-rules'); },
|
||||
'click .js-rules-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-rules'); },
|
||||
'click .js-boards-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-boards'); },
|
||||
'click .js-boards-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-boards'); },
|
||||
'click .js-cards-prev-page'(event, tmpl) { goPrevPage(event, tmpl, 'report-cards'); },
|
||||
'click .js-cards-next-page'(event, tmpl) { goNextPage(event, tmpl, 'report-cards'); },
|
||||
|
||||
// --- Search (one input + button per report, mirrors the People panel) ---
|
||||
'click .js-files-search-button'(event, tmpl) { runSearch(tmpl, 'report-files', '.js-files-search-input'); },
|
||||
'keydown .js-files-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-files', '.js-files-search-input'); },
|
||||
'click .js-rules-search-button'(event, tmpl) { runSearch(tmpl, 'report-rules', '.js-rules-search-input'); },
|
||||
'keydown .js-rules-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-rules', '.js-rules-search-input'); },
|
||||
'click .js-boards-search-button'(event, tmpl) { runSearch(tmpl, 'report-boards', '.js-boards-search-input'); },
|
||||
'keydown .js-boards-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-boards', '.js-boards-search-input'); },
|
||||
'click .js-cards-search-button'(event, tmpl) { runSearch(tmpl, 'report-cards', '.js-cards-search-input'); },
|
||||
'keydown .js-cards-search-input'(event, tmpl) { if (event.keyCode === 13 && !event.shiftKey) runSearch(tmpl, 'report-cards', '.js-cards-search-input'); },
|
||||
});
|
||||
|
||||
function goPrevPage(event, tmpl, reportId) {
|
||||
event.preventDefault();
|
||||
const cfg = reportConfig(tmpl)[reportId];
|
||||
const current = cfg.page.get();
|
||||
if (current > 1) {
|
||||
cfg.page.set(current - 1);
|
||||
tmpl.loadReport(reportId);
|
||||
}
|
||||
}
|
||||
|
||||
function goNextPage(event, tmpl, reportId) {
|
||||
event.preventDefault();
|
||||
const cfg = reportConfig(tmpl)[reportId];
|
||||
const total = cfg.count.get() || 0;
|
||||
const totalPages = Math.max(1, Math.ceil(total / REPORTS_PER_PAGE));
|
||||
const current = cfg.page.get();
|
||||
if (current < totalPages) {
|
||||
cfg.page.set(current + 1);
|
||||
tmpl.loadReport(reportId);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the report's search box, store the term, reset to page 1 and reload.
|
||||
function runSearch(tmpl, reportId, inputSelector) {
|
||||
const cfg = reportConfig(tmpl)[reportId];
|
||||
const value = (tmpl.$(inputSelector).val() || '').trim();
|
||||
cfg.search.set(value);
|
||||
cfg.page.set(1);
|
||||
tmpl.loadReport(reportId);
|
||||
}
|
||||
|
||||
function switchMenu(event, tmpl) {
|
||||
const target = $(event.target);
|
||||
if (!target.hasClass('active')) {
|
||||
@@ -117,6 +274,7 @@ function switchMenu(event, tmpl) {
|
||||
$('.side-menu li.active').removeClass('active');
|
||||
target.parent().addClass('active');
|
||||
const targetID = target.data('id');
|
||||
tmpl.activeReport.set(targetID);
|
||||
|
||||
if ('report-broken' === targetID) {
|
||||
tmpl.showBrokenCardsReport.set(true);
|
||||
@@ -129,32 +287,24 @@ function switchMenu(event, tmpl) {
|
||||
);
|
||||
} else if ('report-files' === targetID) {
|
||||
tmpl.showFilesReport.set(true);
|
||||
tmpl.subscription = Meteor.subscribe('attachmentsList', () => {
|
||||
tmpl.loading.set(false);
|
||||
});
|
||||
tmpl.filesPage.set(1);
|
||||
tmpl.filesSearch.set('');
|
||||
tmpl.loadReport('report-files');
|
||||
} else if ('report-rules' === targetID) {
|
||||
tmpl.subscription = Meteor.subscribe('rulesReport', () => {
|
||||
tmpl.showRulesReport.set(true);
|
||||
tmpl.loading.set(false);
|
||||
});
|
||||
tmpl.showRulesReport.set(true);
|
||||
tmpl.rulesPage.set(1);
|
||||
tmpl.rulesSearch.set('');
|
||||
tmpl.loadReport('report-rules');
|
||||
} else if ('report-boards' === targetID) {
|
||||
tmpl.subscription = Meteor.subscribe('boardsReport', () => {
|
||||
tmpl.showBoardsReport.set(true);
|
||||
tmpl.loading.set(false);
|
||||
});
|
||||
tmpl.showBoardsReport.set(true);
|
||||
tmpl.boardsPage.set(1);
|
||||
tmpl.boardsSearch.set('');
|
||||
tmpl.loadReport('report-boards');
|
||||
} else if ('report-cards' === targetID) {
|
||||
const qp = new QueryParams();
|
||||
qp.addPredicate(OPERATOR_LIMIT, 300);
|
||||
tmpl.subscription = Meteor.subscribe(
|
||||
'globalSearch',
|
||||
tmpl.sessionId,
|
||||
qp.getParams(),
|
||||
qp.text,
|
||||
() => {
|
||||
tmpl.showCardsReport.set(true);
|
||||
tmpl.loading.set(false);
|
||||
},
|
||||
);
|
||||
tmpl.showCardsReport.set(true);
|
||||
tmpl.cardsPage.set(1);
|
||||
tmpl.cardsSearch.set('');
|
||||
tmpl.loadReport('report-cards');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +313,7 @@ function switchMenu(event, tmpl) {
|
||||
|
||||
Template.filesReport.helpers({
|
||||
results() {
|
||||
return collectionResults(Attachments);
|
||||
return collectionResults(Attachments, { name: 1 });
|
||||
},
|
||||
resultsCount() {
|
||||
return collectionResultsCount(Attachments);
|
||||
@@ -179,19 +329,17 @@ Template.rulesReport.helpers({
|
||||
results() {
|
||||
const rules = [];
|
||||
|
||||
ReactiveCache.getRules().forEach(rule => {
|
||||
ReactiveCache.getRules({}, { sort: { boardId: 1 } }).forEach(rule => {
|
||||
rules.push({
|
||||
_id: rule._id,
|
||||
title: rule.title,
|
||||
boardId: rule.boardId,
|
||||
boardTitle: rule.board().title,
|
||||
boardTitle: rule.board()?.title,
|
||||
action: rule.action(),
|
||||
trigger: rule.trigger(),
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rows:', rules);
|
||||
return rules;
|
||||
},
|
||||
resultsCount() {
|
||||
@@ -203,7 +351,7 @@ Template.rulesReport.helpers({
|
||||
|
||||
Template.boardsReport.helpers({
|
||||
results() {
|
||||
return collectionResults(Boards);
|
||||
return collectionResults(Boards, { sort: 1 });
|
||||
},
|
||||
resultsCount() {
|
||||
return collectionResultsCount(Boards);
|
||||
@@ -247,7 +395,7 @@ Template.boardsReport.helpers({
|
||||
|
||||
Template.cardsReport.helpers({
|
||||
results() {
|
||||
return collectionResults(Cards);
|
||||
return collectionResults(Cards, { boardId: 1, sort: 1 });
|
||||
},
|
||||
resultsCount() {
|
||||
return collectionResultsCount(Cards);
|
||||
|
||||
@@ -204,3 +204,39 @@
|
||||
white-space: nowrap;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* Cloud storage (S3 / Azure / GCS): example value shown below each input. */
|
||||
.attachments-content .cloud-input-example {
|
||||
display: block;
|
||||
color: #888;
|
||||
font-size: 0.82em;
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Cloud storage: where-to-find-this-value path shown below the example. */
|
||||
.attachments-content .cloud-input-path {
|
||||
display: block;
|
||||
color: #6a737d;
|
||||
font-size: 0.82em;
|
||||
line-height: 1.4;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Cloud storage: translated label shown under the (English) label. */
|
||||
.attachments-content .cloud-input-label-tr {
|
||||
display: block;
|
||||
color: #57606a;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Cloud storage: description text (English literal and translated). */
|
||||
.attachments-content .cloud-input-desc {
|
||||
display: block;
|
||||
color: #6a737d;
|
||||
font-size: 0.82em;
|
||||
line-height: 1.4;
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -173,6 +173,7 @@ template(name="attachments")
|
||||
ul#attachment-gridfs-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'mongodb-gridfs-storage'}}
|
||||
.description {{_ 'gridfs-move-collectionfs-note'}}
|
||||
.storage-rw-row
|
||||
.storage-rw-item
|
||||
input.js-toggle-gridfs-read#gridfs-read(type="checkbox" checked="{{gridfsRead}}")
|
||||
@@ -229,35 +230,7 @@ template(name="attachments")
|
||||
label(for="s3-read") {{_ 'storage-read'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value=cloudValue.s3.endpoint)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value=cloudValue.s3.region)
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value=cloudValue.s3.bucket)
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" value=cloudValue.s3.accessKeyId)
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder=cloudSecretPlaceholder.s3.secretAccessKey)
|
||||
small.form-text.text-muted {{_ 'cloud-secret-keep-blank'}}
|
||||
.form-group
|
||||
.storage-rw-item
|
||||
input#s3-force-path-style(type="checkbox" checked=cloudValue.s3.forcePathStyle)
|
||||
label(for="s3-force-path-style") {{_ 's3-force-path-style'}}
|
||||
small.form-text.text-muted {{_ 's3-force-path-style-description'}}
|
||||
|
||||
.form-group
|
||||
button.js-test-cloud-connection.btn.btn-secondary(data-provider="s3") {{_ 'test-cloud-connection'}}
|
||||
button.js-save-cloud-settings.btn.btn-primary(data-provider="s3") {{_ 'save'}}
|
||||
if cloudTestResult.s3
|
||||
p.form-text.text-success {{_ 'cloud-connection-success'}}
|
||||
if cloudTestError.s3
|
||||
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.s3}}
|
||||
|
||||
.form-group
|
||||
button.js-calculate-s3-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
|
||||
@@ -277,6 +250,65 @@ template(name="attachments")
|
||||
td {{_ 'avatars'}}
|
||||
td {{s3Stats.avatars}}
|
||||
|
||||
.form-group
|
||||
label Endpoint
|
||||
small.form-text.cloud-input-label-tr {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value=cloudValue.s3.endpoint)
|
||||
small.form-text.cloud-input-example https://s3.eu-west-1.amazonaws.com
|
||||
small.form-text.cloud-input-desc S3 endpoint URL (e.g., s3.amazonaws.com or minio.example.com)
|
||||
small.form-text.cloud-input-desc {{_ 's3-endpoint-description'}}
|
||||
small.form-text.cloud-input-path AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.
|
||||
small.form-text.cloud-input-path {{_ 's3-endpoint-menu-path'}}
|
||||
.form-group
|
||||
label AWS Region
|
||||
small.form-text.cloud-input-label-tr {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value=cloudValue.s3.region)
|
||||
small.form-text.cloud-input-example eu-west-1
|
||||
small.form-text.cloud-input-desc AWS S3 region (e.g., us-east-1)
|
||||
small.form-text.cloud-input-desc {{_ 's3-region-description'}}
|
||||
small.form-text.cloud-input-path AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.
|
||||
small.form-text.cloud-input-path {{_ 's3-region-menu-path'}}
|
||||
.form-group
|
||||
label Bucket name
|
||||
small.form-text.cloud-input-label-tr {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value=cloudValue.s3.bucket)
|
||||
small.form-text.cloud-input-example wekan-s3-test-369341155299-eu-west-1-an
|
||||
small.form-text.cloud-input-desc S3 bucket name for storing files
|
||||
small.form-text.cloud-input-desc {{_ 's3-bucket-description'}}
|
||||
small.form-text.cloud-input-path AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).
|
||||
small.form-text.cloud-input-path {{_ 's3-bucket-menu-path'}}
|
||||
.form-group
|
||||
label Access key ID
|
||||
small.form-text.cloud-input-label-tr {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" value=cloudValue.s3.accessKeyId)
|
||||
small.form-text.cloud-input-example AKIAIOSFODNN7EXAMPLE
|
||||
small.form-text.cloud-input-desc AWS S3 access key for authentication
|
||||
small.form-text.cloud-input-desc {{_ 's3-access-key-description'}}
|
||||
small.form-text.cloud-input-path AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.
|
||||
small.form-text.cloud-input-path {{_ 's3-access-key-menu-path'}}
|
||||
.form-group
|
||||
label Secret access key
|
||||
small.form-text.cloud-input-label-tr {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder=cloudSecretPlaceholder.s3.secretAccessKey)
|
||||
small.form-text.cloud-input-example wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
small.form-text.cloud-input-desc Leave blank to keep the current value.
|
||||
small.form-text.cloud-input-desc {{_ 'cloud-secret-keep-blank'}}
|
||||
small.form-text.cloud-input-path Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).
|
||||
small.form-text.cloud-input-path {{_ 's3-secret-key-menu-path'}}
|
||||
.form-group
|
||||
.storage-rw-item
|
||||
input#s3-force-path-style(type="checkbox" checked=cloudValue.s3.forcePathStyle)
|
||||
label(for="s3-force-path-style") {{_ 's3-force-path-style'}}
|
||||
small.form-text.text-muted {{_ 's3-force-path-style-description'}}
|
||||
|
||||
.form-group
|
||||
button.js-test-cloud-connection.btn.btn-secondary(data-provider="s3") {{_ 'test-cloud-connection'}}
|
||||
button.js-save-cloud-settings.btn.btn-primary(data-provider="s3") {{_ 'save'}}
|
||||
if cloudTestResult.s3
|
||||
p.form-text.text-success {{_ 'cloud-connection-success'}}
|
||||
if cloudTestError.s3
|
||||
p.form-text.text-danger {{_ 'cloud-connection-failed'}}: {{cloudTestError.s3}}
|
||||
|
||||
else if isAzureActive
|
||||
ul#attachment-azure-setting.setting-detail
|
||||
li
|
||||
@@ -290,19 +322,62 @@ template(name="attachments")
|
||||
label(for="azure-read") {{_ 'storage-read'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'azure-account-name'}}
|
||||
button.js-save-cloud-settings.btn.btn-primary(data-provider="azure") {{_ 'save'}}
|
||||
|
||||
.form-group
|
||||
button.js-calculate-azure-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
|
||||
if azureStatsLoading
|
||||
p.form-text.text-muted {{_ 'calculating-counts'}}
|
||||
if azureStatsError
|
||||
p.form-text.text-danger {{azureStatsError}}
|
||||
if hasAzureStats
|
||||
table.gridfs-stats-table
|
||||
tr
|
||||
th {{_ 'stats-scope'}}
|
||||
th {{_ 'stats-count'}}
|
||||
tr
|
||||
td {{_ 'attachments'}}
|
||||
td {{azureStats.attachments}}
|
||||
tr
|
||||
td {{_ 'avatars'}}
|
||||
td {{azureStats.avatars}}
|
||||
|
||||
.form-group
|
||||
label Storage account name
|
||||
small.form-text.cloud-input-label-tr {{_ 'azure-account-name'}}
|
||||
input.wekan-form-control#azure-account-name(type="text" value=cloudValue.azure.accountName)
|
||||
small.form-text.cloud-input-example wekanstorage
|
||||
small.form-text.cloud-input-desc The name of your Azure Storage account.
|
||||
small.form-text.cloud-input-desc {{_ 'azure-account-name-description'}}
|
||||
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).
|
||||
small.form-text.cloud-input-path {{_ 'azure-account-name-menu-path'}}
|
||||
.form-group
|
||||
label {{_ 'azure-account-key'}}
|
||||
label Storage account key
|
||||
small.form-text.cloud-input-label-tr {{_ 'azure-account-key'}}
|
||||
input.wekan-form-control#azure-account-key(type="password" placeholder=cloudSecretPlaceholder.azure.accountKey)
|
||||
small.form-text.text-muted {{_ 'cloud-secret-keep-blank'}}
|
||||
small.form-text.cloud-input-example base64AccountKey==
|
||||
small.form-text.cloud-input-desc Leave blank to keep the current value.
|
||||
small.form-text.cloud-input-desc {{_ 'cloud-secret-keep-blank'}}
|
||||
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.
|
||||
small.form-text.cloud-input-path {{_ 'azure-account-key-menu-path'}}
|
||||
.form-group
|
||||
label {{_ 'azure-connection-string'}}
|
||||
label Connection string
|
||||
small.form-text.cloud-input-label-tr {{_ 'azure-connection-string'}}
|
||||
input.wekan-form-control#azure-connection-string(type="password" placeholder=cloudSecretPlaceholder.azure.connectionString)
|
||||
small.form-text.text-muted {{_ 'azure-connection-string-description'}}
|
||||
small.form-text.cloud-input-example DefaultEndpointsProtocol=https;AccountName=wekanstorage;AccountKey=...;EndpointSuffix=core.windows.net
|
||||
small.form-text.cloud-input-desc Optional: use a full connection string instead of account name and key.
|
||||
small.form-text.cloud-input-desc {{_ 'azure-connection-string-description'}}
|
||||
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.
|
||||
small.form-text.cloud-input-path {{_ 'azure-connection-string-menu-path'}}
|
||||
.form-group
|
||||
label {{_ 'azure-container'}}
|
||||
label Container name
|
||||
small.form-text.cloud-input-label-tr {{_ 'azure-container'}}
|
||||
input.wekan-form-control#azure-bucket(type="text" value=cloudValue.azure.bucket)
|
||||
small.form-text.cloud-input-example wekan-attachments
|
||||
small.form-text.cloud-input-desc The blob container to store files in.
|
||||
small.form-text.cloud-input-desc {{_ 'azure-container-description'}}
|
||||
small.form-text.cloud-input-path Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).
|
||||
small.form-text.cloud-input-path {{_ 'azure-container-menu-path'}}
|
||||
|
||||
.form-group
|
||||
button.js-test-cloud-connection.btn.btn-secondary(data-provider="azure") {{_ 'test-cloud-connection'}}
|
||||
@@ -316,6 +391,8 @@ template(name="attachments")
|
||||
ul#attachment-gcs-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'gcs-storage'}}
|
||||
.description Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In "New principals", paste the service account email — the "client_email" value from your JSON key — then choose the role "Storage Object Admin" and Save.
|
||||
.description {{_ 'gcs-permissions-note'}}
|
||||
.storage-rw-row
|
||||
.storage-rw-item
|
||||
input#gcs-enabled(type="checkbox" checked=cloudEnabled.gcs)
|
||||
@@ -325,18 +402,62 @@ template(name="attachments")
|
||||
label(for="gcs-read") {{_ 'storage-read'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'gcs-project-id'}}
|
||||
button.js-save-cloud-settings.btn.btn-primary(data-provider="gcs") {{_ 'save'}}
|
||||
|
||||
.form-group
|
||||
button.js-calculate-gcs-stats.btn.btn-primary(type="button") {{_ 'calculate-file-counts'}}
|
||||
if gcsStatsLoading
|
||||
p.form-text.text-muted {{_ 'calculating-counts'}}
|
||||
if gcsStatsError
|
||||
p.form-text.text-danger {{gcsStatsError}}
|
||||
if hasGcsStats
|
||||
table.gridfs-stats-table
|
||||
tr
|
||||
th {{_ 'stats-scope'}}
|
||||
th {{_ 'stats-count'}}
|
||||
tr
|
||||
td {{_ 'attachments'}}
|
||||
td {{gcsStats.attachments}}
|
||||
tr
|
||||
td {{_ 'avatars'}}
|
||||
td {{gcsStats.avatars}}
|
||||
|
||||
.form-group
|
||||
label Project ID
|
||||
small.form-text.cloud-input-label-tr {{_ 'gcs-project-id'}}
|
||||
input.wekan-form-control#gcs-project-id(type="text" value=cloudValue.gcs.projectId)
|
||||
small.form-text.cloud-input-example my-gcp-project-123456
|
||||
small.form-text.cloud-input-desc Your Google Cloud project id.
|
||||
small.form-text.cloud-input-desc {{_ 'gcs-project-id-description'}}
|
||||
small.form-text.cloud-input-path Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).
|
||||
small.form-text.cloud-input-path {{_ 'gcs-project-id-menu-path'}}
|
||||
.form-group
|
||||
label {{_ 'gcs-bucket'}}
|
||||
label Bucket name
|
||||
small.form-text.cloud-input-label-tr {{_ 'gcs-bucket'}}
|
||||
input.wekan-form-control#gcs-bucket(type="text" value=cloudValue.gcs.bucket)
|
||||
small.form-text.cloud-input-example wekan-attachments
|
||||
small.form-text.cloud-input-desc The name of the Cloud Storage bucket to store files in.
|
||||
small.form-text.cloud-input-desc {{_ 'gcs-bucket-description'}}
|
||||
small.form-text.cloud-input-path Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).
|
||||
small.form-text.cloud-input-path {{_ 'gcs-bucket-menu-path'}}
|
||||
.form-group
|
||||
label {{_ 'gcs-key-filename'}}
|
||||
label Service account key file path
|
||||
small.form-text.cloud-input-label-tr {{_ 'gcs-key-filename'}}
|
||||
input.wekan-form-control#gcs-key-filename(type="text" value=cloudValue.gcs.keyFilename)
|
||||
small.form-text.text-muted {{_ 'gcs-key-filename-description'}}
|
||||
small.form-text.cloud-input-example /data/gcs-service-account.json
|
||||
small.form-text.cloud-input-desc Optional: path to a service account JSON key file on the server.
|
||||
small.form-text.cloud-input-desc {{_ 'gcs-key-filename-description'}}
|
||||
small.form-text.cloud-input-path Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.
|
||||
small.form-text.cloud-input-path {{_ 'gcs-key-filename-menu-path'}}
|
||||
.form-group
|
||||
label {{_ 'gcs-credentials'}}
|
||||
label Service account key (JSON)
|
||||
small.form-text.cloud-input-label-tr {{_ 'gcs-credentials'}}
|
||||
textarea.wekan-form-control#gcs-credentials(rows="4" placeholder=cloudSecretPlaceholder.gcs.credentials)
|
||||
small.form-text.cloud-input-example { "type": "service_account", "project_id": "my-gcp-project", "client_email": "wekan@my-gcp-project.iam.gserviceaccount.com", "private_key": "-----BEGIN PRIVATE KEY-----..." }
|
||||
small.form-text.cloud-input-desc Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.
|
||||
small.form-text.cloud-input-desc {{_ 'gcs-credentials-description'}}
|
||||
small.form-text.cloud-input-path Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.
|
||||
small.form-text.cloud-input-path {{_ 'gcs-credentials-menu-path'}}
|
||||
small.form-text.text-muted {{_ 'gcs-credentials-description'}}
|
||||
|
||||
.form-group
|
||||
@@ -350,9 +471,16 @@ template(name="attachments")
|
||||
template(name="moveAttachments")
|
||||
.move-storage-form
|
||||
.move-storage-selects
|
||||
.form-group
|
||||
label {{_ 'move-scope'}}
|
||||
select.wekan-form-control.js-move-scope
|
||||
option(value="attachments") {{_ 'move-scope-attachments'}}
|
||||
option(value="avatars") {{_ 'move-scope-avatars'}}
|
||||
option(value="both") {{_ 'move-scope-both'}}
|
||||
.form-group
|
||||
label {{_ 'move-source'}}
|
||||
select.wekan-form-control.js-move-source-storage
|
||||
option(value="all") {{_ 'move-storage-all'}}
|
||||
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
|
||||
option(value="gridfs") {{_ 'move-storage-gridfs'}}
|
||||
option(value="fs") {{_ 'move-storage-fs'}}
|
||||
@@ -370,6 +498,39 @@ template(name="moveAttachments")
|
||||
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
|
||||
button.js-move-all-attachments.btn.btn-primary {{_ 'move-all-attachments'}}
|
||||
|
||||
if lastMove
|
||||
.move-last-operation
|
||||
span.emoji-icon
|
||||
i.fa.fa-info-circle
|
||||
| {{_ 'attachment-last-move'}}: {{lastMoveText}}
|
||||
|
||||
.move-repair-section
|
||||
h4 {{_ 'attachment-repair-locations'}}
|
||||
.description {{_ 'attachment-repair-locations-description'}}
|
||||
button.js-repair-attachment-locations.btn.btn-secondary(type="button") {{_ 'attachment-repair-locations'}}
|
||||
if repairLoading
|
||||
p.form-text.text-muted {{_ 'attachment-repair-running'}}
|
||||
if repairError
|
||||
p.form-text.text-danger {{repairError}}
|
||||
if repairResult
|
||||
p.form-text.text-success {{_ 'attachment-repair-done'}}
|
||||
table.gridfs-stats-table
|
||||
tr
|
||||
th {{_ 'stats-scope'}}
|
||||
th {{_ 'attachment-repair-scanned'}}
|
||||
th {{_ 'attachment-repair-repaired'}}
|
||||
th {{_ 'attachment-repair-broken'}}
|
||||
tr
|
||||
td {{_ 'attachments'}}
|
||||
td {{repairResult.attachments.scanned}}
|
||||
td {{repairResult.attachments.repaired}}
|
||||
td {{repairResult.attachments.broken}}
|
||||
tr
|
||||
td {{_ 'avatars'}}
|
||||
td {{repairResult.avatars.scanned}}
|
||||
td {{repairResult.avatars.repaired}}
|
||||
td {{repairResult.avatars.broken}}
|
||||
|
||||
if moveProgress
|
||||
.move-progress-section
|
||||
.move-progress-header
|
||||
@@ -386,98 +547,3 @@ template(name="moveAttachments")
|
||||
else
|
||||
button.js-pause-move.btn.btn-secondary {{_ 'move-progress-pause'}}
|
||||
button.js-cancel-move.btn.btn-danger {{_ 'move-progress-cancel'}}
|
||||
|
||||
each board in getBoardsWithAttachments
|
||||
+moveBoardAttachments board
|
||||
|
||||
template(name="moveBoardAttachments")
|
||||
hr
|
||||
.board-description
|
||||
table
|
||||
tr
|
||||
th {{_ 'board-id'}}
|
||||
th {{_ 'board-title'}}
|
||||
tr
|
||||
td {{ _id }}
|
||||
td {{ title }}
|
||||
|
||||
.move-storage-form
|
||||
.move-storage-selects
|
||||
.form-group
|
||||
label {{_ 'move-source'}}
|
||||
select.wekan-form-control.js-move-board-source-storage
|
||||
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
|
||||
option(value="gridfs") {{_ 'move-storage-gridfs'}}
|
||||
option(value="fs") {{_ 'move-storage-fs'}}
|
||||
option(value="s3") {{_ 'move-storage-s3'}}
|
||||
option(value="azure") {{_ 'move-storage-azure'}}
|
||||
option(value="gcs") {{_ 'move-storage-gcs'}}
|
||||
.form-group
|
||||
label {{_ 'move-destination'}}
|
||||
select.wekan-form-control.js-move-board-dest-storage
|
||||
option(value="gridfs") {{_ 'move-storage-gridfs'}}
|
||||
option(value="fs") {{_ 'move-storage-fs'}}
|
||||
option(value="s3") {{_ 'move-storage-s3'}}
|
||||
option(value="azure") {{_ 'move-storage-azure'}}
|
||||
option(value="gcs") {{_ 'move-storage-gcs'}}
|
||||
option(value="collectionfs") {{_ 'move-storage-collectionfs'}}
|
||||
button.js-move-all-attachments-of-board.btn.btn-primary {{_ 'move-all-attachments'}}
|
||||
|
||||
.board-attachments
|
||||
table
|
||||
tr
|
||||
th {{_ 'card-id'}}
|
||||
th {{_ 'attachment-id'}}
|
||||
th {{_ 'name'}}
|
||||
th {{_ 'path'}}
|
||||
th {{_ 'version-name'}}
|
||||
th {{_ 'size'}}
|
||||
th {{_ 'gridfs-file-id'}}
|
||||
th {{_ 's3-file-id'}}
|
||||
th {{_ 'storage'}}
|
||||
th {{_ 'action'}}
|
||||
|
||||
each attachment in attachments
|
||||
+moveAttachment attachment
|
||||
|
||||
template(name="moveAttachment")
|
||||
each version in flatVersion
|
||||
tr
|
||||
td {{ meta.cardId }}
|
||||
td {{ _id }}
|
||||
td {{ name }}
|
||||
td {{ version.path }}
|
||||
td {{ version.versionName }}
|
||||
td {{ fileSize version.size }}
|
||||
td {{ version.meta.gridFsFileId }}
|
||||
td {{ version.meta.s3FileId }}
|
||||
td {{ version.storageName }}
|
||||
td
|
||||
if $neq version.storageName "fs"
|
||||
button.js-move-storage-fs
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-fs'}}
|
||||
|
||||
if $neq version.storageName "gridfs"
|
||||
if version.storageName
|
||||
button.js-move-storage-gridfs
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-gridfs'}}
|
||||
|
||||
if $neq version.storageName "s3"
|
||||
if version.storageName
|
||||
button.js-move-storage-s3
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-s3'}}
|
||||
|
||||
if $neq version.storageName "azure"
|
||||
if version.storageName
|
||||
button.js-move-storage-azure
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-azure'}}
|
||||
|
||||
if $neq version.storageName "gcs"
|
||||
if version.storageName
|
||||
button.js-move-storage-gcs
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-gcs'}}
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { groupBy } from '/imports/lib/collectionHelpers';
|
||||
import Attachments from '/models/attachments';
|
||||
import AttachmentStorageSettings from '/models/attachmentStorageSettings';
|
||||
import AttachmentBulkMoveStatus from '/models/attachmentBulkMoveStatus';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
@@ -47,7 +43,10 @@ function gatherCloudConfig(tpl, provider) {
|
||||
if (el.attr('type') === 'checkbox') {
|
||||
cfg[field] = el.prop('checked');
|
||||
} else {
|
||||
cfg[field] = el.val();
|
||||
// Trim so stray whitespace/newlines from copy-paste (e.g. an account name
|
||||
// or endpoint) do not produce an invalid URL in the storage adapter.
|
||||
const value = el.val();
|
||||
cfg[field] = typeof value === 'string' ? value.trim() : value;
|
||||
}
|
||||
});
|
||||
return cfg;
|
||||
@@ -231,7 +230,6 @@ function refreshAttachmentStorageSettings(tpl) {
|
||||
|
||||
Template.attachments.onCreated(function () {
|
||||
this.activeSection = new ReactiveVar('move');
|
||||
this.attachmentsSubscription = Meteor.subscribe('attachmentsList');
|
||||
this.storageSettingsSubscription = Meteor.subscribe('attachmentStorageSettings');
|
||||
this.attachmentStorageSettings = new ReactiveVar(null);
|
||||
this.attachmentLimitUnits = new ReactiveVar({
|
||||
@@ -255,6 +253,12 @@ Template.attachments.onCreated(function () {
|
||||
this.s3Stats = new ReactiveVar(null);
|
||||
this.s3StatsLoading = new ReactiveVar(false);
|
||||
this.s3StatsError = new ReactiveVar('');
|
||||
this.azureStats = new ReactiveVar(null);
|
||||
this.azureStatsLoading = new ReactiveVar(false);
|
||||
this.azureStatsError = new ReactiveVar('');
|
||||
this.gcsStats = new ReactiveVar(null);
|
||||
this.gcsStatsLoading = new ReactiveVar(false);
|
||||
this.gcsStatsError = new ReactiveVar('');
|
||||
this.compactLoading = new ReactiveVar(false);
|
||||
this.compactResult = new ReactiveVar(null);
|
||||
this.compactError = new ReactiveVar('');
|
||||
@@ -263,7 +267,7 @@ Template.attachments.onCreated(function () {
|
||||
this.loading = new ReactiveVar(false);
|
||||
|
||||
this.autorun(() => {
|
||||
const ready = this.attachmentsSubscription.ready() && this.storageSettingsSubscription.ready();
|
||||
const ready = this.storageSettingsSubscription.ready();
|
||||
if (ready) {
|
||||
refreshAttachmentStorageSettings(this);
|
||||
this.loading.set(false);
|
||||
@@ -472,6 +476,30 @@ Template.attachments.helpers({
|
||||
s3StatsError() {
|
||||
return Template.instance().s3StatsError.get();
|
||||
},
|
||||
hasAzureStats() {
|
||||
return !!Template.instance().azureStats.get();
|
||||
},
|
||||
azureStats() {
|
||||
return Template.instance().azureStats.get();
|
||||
},
|
||||
azureStatsLoading() {
|
||||
return Template.instance().azureStatsLoading.get();
|
||||
},
|
||||
azureStatsError() {
|
||||
return Template.instance().azureStatsError.get();
|
||||
},
|
||||
hasGcsStats() {
|
||||
return !!Template.instance().gcsStats.get();
|
||||
},
|
||||
gcsStats() {
|
||||
return Template.instance().gcsStats.get();
|
||||
},
|
||||
gcsStatsLoading() {
|
||||
return Template.instance().gcsStatsLoading.get();
|
||||
},
|
||||
gcsStatsError() {
|
||||
return Template.instance().gcsStatsError.get();
|
||||
},
|
||||
s3Enabled() {
|
||||
return process.env.S3_ENABLED === 'true';
|
||||
},
|
||||
@@ -725,6 +753,34 @@ Template.attachments.events({
|
||||
tpl.s3Stats.set(result || null);
|
||||
});
|
||||
},
|
||||
'click button.js-calculate-azure-stats'(event, tpl) {
|
||||
event.preventDefault();
|
||||
tpl.azureStatsLoading.set(true);
|
||||
tpl.azureStatsError.set('');
|
||||
Meteor.call('getAzureStorageStats', (error, result) => {
|
||||
tpl.azureStatsLoading.set(false);
|
||||
if (error) {
|
||||
tpl.azureStats.set(null);
|
||||
tpl.azureStatsError.set(error.reason || error.message || 'Failed to calculate counts');
|
||||
return;
|
||||
}
|
||||
tpl.azureStats.set(result || null);
|
||||
});
|
||||
},
|
||||
'click button.js-calculate-gcs-stats'(event, tpl) {
|
||||
event.preventDefault();
|
||||
tpl.gcsStatsLoading.set(true);
|
||||
tpl.gcsStatsError.set('');
|
||||
Meteor.call('getGcsStorageStats', (error, result) => {
|
||||
tpl.gcsStatsLoading.set(false);
|
||||
if (error) {
|
||||
tpl.gcsStats.set(null);
|
||||
tpl.gcsStatsError.set(error.reason || error.message || 'Failed to calculate counts');
|
||||
return;
|
||||
}
|
||||
tpl.gcsStats.set(result || null);
|
||||
});
|
||||
},
|
||||
'click button.js-compact-mongodb-gridfs'(event, tpl) {
|
||||
event.preventDefault();
|
||||
runCompact(tpl.compactLoading, tpl.compactResult, tpl.compactError);
|
||||
@@ -732,11 +788,13 @@ Template.attachments.events({
|
||||
});
|
||||
|
||||
Template.moveAttachments.onCreated(function () {
|
||||
this.attachments = null;
|
||||
// The bulk move runs as a server-side background job; subscribe to its
|
||||
// persisted progress so it keeps running (and stays visible) even if the
|
||||
// admin navigates away from or closes this page.
|
||||
this.bulkMoveSubscription = Meteor.subscribe('attachmentBulkMoveStatus');
|
||||
this.repairLoading = new ReactiveVar(false);
|
||||
this.repairResult = new ReactiveVar(null);
|
||||
this.repairError = new ReactiveVar('');
|
||||
});
|
||||
|
||||
function getBulkMoveProgress() {
|
||||
@@ -744,6 +802,31 @@ function getBulkMoveProgress() {
|
||||
return doc && doc.running ? doc : null;
|
||||
}
|
||||
|
||||
function getLastMove() {
|
||||
const doc = AttachmentBulkMoveStatus.findOne('bulk');
|
||||
// Don't show the "last move" line while a move is actively running.
|
||||
if (!doc || doc.running || !doc.lastMove) return null;
|
||||
return doc.lastMove;
|
||||
}
|
||||
|
||||
function storageLabel(value) {
|
||||
return value ? TAPi18n.__(`move-storage-${value}`) : '';
|
||||
}
|
||||
|
||||
function scopeLabel(value) {
|
||||
return value ? TAPi18n.__(`move-scope-${value}`) : '';
|
||||
}
|
||||
|
||||
// Format a Date (or value) as YYYY-MM-DD HH:MM:SS in local time.
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '';
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
const p = n => String(n).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ` +
|
||||
`${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
Template.moveAttachments.helpers({
|
||||
moveProgress() {
|
||||
return getBulkMoveProgress();
|
||||
@@ -760,41 +843,24 @@ Template.moveAttachments.helpers({
|
||||
const p = getBulkMoveProgress();
|
||||
return p?.size ? filesize(p.size) : '';
|
||||
},
|
||||
getBoardsWithAttachments() {
|
||||
const tpl = Template.instance();
|
||||
tpl.attachments = ReactiveCache.getAttachments();
|
||||
const attachmentsByBoardId = groupBy(tpl.attachments, fileObj => fileObj.meta?.boardId);
|
||||
|
||||
const ret = Object.keys(attachmentsByBoardId)
|
||||
.map(boardId => {
|
||||
const boardAttachments = attachmentsByBoardId[boardId];
|
||||
|
||||
boardAttachments.forEach(_attachment => {
|
||||
_attachment.flatVersion = Object.keys(_attachment.versions || {})
|
||||
.map(_versionName => {
|
||||
const _version = Object.assign(_attachment.versions[_versionName], {"versionName": _versionName});
|
||||
// Read storage directly from the document (set by onAfterUpload on server)
|
||||
_version.storageName = _version.storage || (
|
||||
(_attachment.meta?.source === 'import' || _version.meta?.gridFsFileId) ? 'gridfs' : 'fs'
|
||||
);
|
||||
return _version;
|
||||
});
|
||||
});
|
||||
// The board may no longer exist (or not be loaded in the cache) for
|
||||
// orphaned attachments. Fall back to a placeholder so the move list
|
||||
// still renders these attachments instead of throwing.
|
||||
const board = ReactiveCache.getBoard(boardId) || {
|
||||
_id: boardId,
|
||||
title: boardId ? `(${boardId})` : '(no board)',
|
||||
};
|
||||
board.attachments = boardAttachments;
|
||||
return board;
|
||||
})
|
||||
return ret;
|
||||
lastMove() {
|
||||
return getLastMove();
|
||||
},
|
||||
getBoardData(boardId) {
|
||||
const ret = ReactiveCache.getBoard(boardId);
|
||||
return ret;
|
||||
lastMoveText() {
|
||||
const lm = getLastMove();
|
||||
if (!lm) return '';
|
||||
const cancelled = lm.cancelled ? ` (${TAPi18n.__('move-progress-cancel')})` : '';
|
||||
return `${storageLabel(lm.source)} → ${storageLabel(lm.dest)} ` +
|
||||
`(${scopeLabel(lm.scope)}) ${formatDateTime(lm.at)}${cancelled}`;
|
||||
},
|
||||
repairLoading() {
|
||||
return Template.instance().repairLoading.get();
|
||||
},
|
||||
repairResult() {
|
||||
return Template.instance().repairResult.get();
|
||||
},
|
||||
repairError() {
|
||||
return Template.instance().repairError.get();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -812,40 +878,43 @@ function runCompact(loadingVar, resultVar, errorVar) {
|
||||
});
|
||||
}
|
||||
|
||||
function attachmentVersionMatchesSource(attachment, source) {
|
||||
const versions = Object.values(attachment.versions || {});
|
||||
return versions.some(v => {
|
||||
switch (source) {
|
||||
case 'collectionfs':
|
||||
return !!(v.meta?.gridFsFileId);
|
||||
case 'gridfs':
|
||||
return v.storage === 'gridfs' && !v.meta?.gridFsFileId;
|
||||
case 'fs':
|
||||
return v.storage === 'fs' || (!v.storage && !v.meta?.gridFsFileId);
|
||||
case 's3':
|
||||
return v.storage === 's3';
|
||||
case 'azure':
|
||||
return v.storage === 'azure';
|
||||
case 'gcs':
|
||||
return v.storage === 'gcs';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Template.moveAttachments.events({
|
||||
'click button.js-move-all-attachments'(event, tpl) {
|
||||
if (getBulkMoveProgress()) return;
|
||||
const scope = tpl.$('.js-move-scope').val() || 'attachments';
|
||||
const source = tpl.$('.js-move-source-storage').val();
|
||||
const dest = tpl.$('.js-move-dest-storage').val();
|
||||
if (!source || !dest || source === dest) return;
|
||||
// 'all' reads from every Read-enabled storage, so it may equal nothing; only
|
||||
// a specific source must differ from the destination.
|
||||
if (!source || !dest || (source !== 'all' && source === dest)) return;
|
||||
// Hand the whole job to the server so the transfer keeps running as a
|
||||
// background process even if this page is left or closed.
|
||||
Meteor.call('startBulkAttachmentMove', source, dest, (error) => {
|
||||
if (error && error.error !== 'bulk-move-already-running') {
|
||||
alert(error.reason || error.message);
|
||||
Meteor.call('startBulkAttachmentMove', source, dest, scope, (error, result) => {
|
||||
if (error) {
|
||||
if (error.error !== 'bulk-move-already-running') {
|
||||
alert(error.reason || error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// No progress bar appears when nothing matches the source, so tell the
|
||||
// admin instead of silently doing nothing.
|
||||
if (result && result.total === 0) {
|
||||
alert(TAPi18n.__('move-attachments-none-found'));
|
||||
}
|
||||
});
|
||||
},
|
||||
'click button.js-repair-attachment-locations'(event, tpl) {
|
||||
if (tpl.repairLoading.get()) return;
|
||||
tpl.repairLoading.set(true);
|
||||
tpl.repairResult.set(null);
|
||||
tpl.repairError.set('');
|
||||
Meteor.call('repairAttachmentStorageLocations', (error, result) => {
|
||||
tpl.repairLoading.set(false);
|
||||
if (error) {
|
||||
tpl.repairError.set(error.reason || error.message || 'Repair failed');
|
||||
return;
|
||||
}
|
||||
tpl.repairResult.set(result || {});
|
||||
});
|
||||
},
|
||||
'click button.js-pause-move'() {
|
||||
@@ -858,48 +927,3 @@ Template.moveAttachments.events({
|
||||
Meteor.call('cancelBulkAttachmentMove');
|
||||
},
|
||||
});
|
||||
|
||||
Template.moveBoardAttachments.events({
|
||||
'click button.js-move-all-attachments-of-board'(event, tpl) {
|
||||
const source = tpl.$('.js-move-board-source-storage').val();
|
||||
const dest = tpl.$('.js-move-board-dest-storage').val();
|
||||
if (!source || !dest || source === dest) return;
|
||||
const destStorage = dest === 'collectionfs' ? 'gridfs' : dest;
|
||||
const data = Template.currentData();
|
||||
(data.attachments || []).forEach(_attachment => {
|
||||
if (attachmentVersionMatchesSource(_attachment, source)) {
|
||||
Meteor.call('moveAttachmentToStorage', _attachment._id, destStorage);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.moveAttachment.helpers({
|
||||
fileSize(size) {
|
||||
const ret = filesize(size);
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
Template.moveAttachment.events({
|
||||
'click button.js-move-storage-fs'() {
|
||||
const data = Template.currentData();
|
||||
Meteor.call('moveAttachmentToStorage', data._id, "fs");
|
||||
},
|
||||
'click button.js-move-storage-gridfs'() {
|
||||
const data = Template.currentData();
|
||||
Meteor.call('moveAttachmentToStorage', data._id, "gridfs");
|
||||
},
|
||||
'click button.js-move-storage-s3'() {
|
||||
const data = Template.currentData();
|
||||
Meteor.call('moveAttachmentToStorage', data._id, "s3");
|
||||
},
|
||||
'click button.js-move-storage-azure'() {
|
||||
const data = Template.currentData();
|
||||
Meteor.call('moveAttachmentToStorage', data._id, "azure");
|
||||
},
|
||||
'click button.js-move-storage-gcs'() {
|
||||
const data = Template.currentData();
|
||||
Meteor.call('moveAttachmentToStorage', data._id, "gcs");
|
||||
},
|
||||
});
|
||||
@@ -275,12 +275,12 @@ Template.people.helpers({
|
||||
},
|
||||
orgList() {
|
||||
const tpl = Template.instance();
|
||||
const page = tpl.orgPage.get();
|
||||
const skipOrgs = (page - 1) * orgsPerPage;
|
||||
// The 'org' publication already returns only the current page (server-side
|
||||
// limit/skip, sorted createdAt:-1). Display exactly what it published:
|
||||
// re-applying skip/limit here paginated an already-paginated set, which
|
||||
// left page 2 with a single stray doc and later pages empty.
|
||||
const orgs = Org.find(tpl.findOrgsOptions.get(), {
|
||||
sort: { orgDisplayName: 1 },
|
||||
skip: skipOrgs,
|
||||
limit: orgsPerPage,
|
||||
sort: { createdAt: -1 },
|
||||
fields: {
|
||||
_id: 1,
|
||||
orgDisplayName: 1,
|
||||
@@ -295,12 +295,12 @@ Template.people.helpers({
|
||||
},
|
||||
teamList() {
|
||||
const tpl = Template.instance();
|
||||
const page = tpl.teamPage.get();
|
||||
const skipTeams = (page - 1) * teamsPerPage;
|
||||
// The 'team' publication already returns only the current page (server-side
|
||||
// limit/skip, sorted createdAt:-1). Display exactly what it published:
|
||||
// re-applying skip/limit here paginated an already-paginated set, which
|
||||
// left page 2 with a single stray doc and later pages empty.
|
||||
const teams = Team.find(tpl.findTeamsOptions.get(), {
|
||||
sort: { teamDisplayName: 1 },
|
||||
skip: skipTeams,
|
||||
limit: teamsPerPage,
|
||||
sort: { createdAt: -1 },
|
||||
fields: {
|
||||
_id: 1,
|
||||
teamDisplayName: 1,
|
||||
@@ -315,12 +315,13 @@ Template.people.helpers({
|
||||
},
|
||||
peopleList() {
|
||||
const tpl = Template.instance();
|
||||
const page = tpl.peoplePage.get();
|
||||
const skipUsers = (page - 1) * usersPerPage;
|
||||
// The 'people' publication already returns only the current page (server-side
|
||||
// limit/skip, sorted createdAt:-1). Display exactly what it published:
|
||||
// re-applying skip/limit here paginated an already-paginated set — that is
|
||||
// why page 1 showed 25, page 2 showed a single stray doc (the admin's own
|
||||
// user record, always present in minimongo) and later pages were empty.
|
||||
const users = Users.find(tpl.findUsersOptions.get(), {
|
||||
sort: { username: 1 },
|
||||
skip: skipUsers,
|
||||
limit: usersPerPage,
|
||||
sort: { createdAt: -1 },
|
||||
fields: {
|
||||
_id: 1,
|
||||
username: 1,
|
||||
@@ -394,7 +395,10 @@ Template.people.helpers({
|
||||
|
||||
Template.people.events({
|
||||
'scroll .main-body'(event, tpl) {
|
||||
if (tpl.peopleSetting.get()) {
|
||||
// Orgs, teams and people all use explicit prev/next pagination (server-side
|
||||
// limit/skip driven by orgPage/teamPage/peoplePage). Infinite scroll must
|
||||
// stay disabled on those tabs so the two paging mechanisms don't fight.
|
||||
if (tpl.orgSetting.get() || tpl.teamSetting.get() || tpl.peopleSetting.get()) {
|
||||
return;
|
||||
}
|
||||
tpl.infiniteScrolling.checkScrollPosition(event.currentTarget, () => {
|
||||
|
||||
@@ -342,3 +342,51 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
|
||||
padding-top: 5px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
/* Board backgrounds management popup */
|
||||
.board-backgrounds-manage .board-backgrounds-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.board-bg-item {
|
||||
width: 48%;
|
||||
margin: 1%;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.board-bg-item.is-active {
|
||||
border-color: #3cb500;
|
||||
}
|
||||
.board-bg-thumb {
|
||||
height: 80px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
}
|
||||
.board-bg-active-check {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
color: #3cb500;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
}
|
||||
.board-bg-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.board-bg-actions a {
|
||||
cursor: pointer;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
.board-bg-actions a:hover {
|
||||
color: #000;
|
||||
}
|
||||
.js-bg-upload-button {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -129,6 +129,38 @@ template(name="boardChangeColorPopup")
|
||||
if isSelected
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="boardBackgroundsPopup")
|
||||
.board-backgrounds-manage
|
||||
input.js-bg-upload-input.hide(type="file" name="file" accept="image/*")
|
||||
a.primary.wide.js-bg-upload-button
|
||||
i.fa.fa-plus
|
||||
= ' '
|
||||
| {{_ 'upload-background'}}
|
||||
if uploading.get
|
||||
+spinner
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
.board-backgrounds-grid.clearfix
|
||||
each backgrounds
|
||||
.board-bg-item(class="{{#if isActiveBackground}}is-active{{/if}}")
|
||||
.board-bg-thumb(style="background-image: url('{{link}}');")
|
||||
if isActiveBackground
|
||||
i.fa.fa-check.board-bg-active-check
|
||||
.board-bg-actions
|
||||
unless isActiveBackground
|
||||
a.js-set-board-background(title="{{_ 'set-as-active'}}")
|
||||
i.fa.fa-check-circle-o
|
||||
a.js-download-board-background(href="{{link}}?download=true" download="{{name}}" title="{{_ 'download'}}")
|
||||
i.fa.fa-arrow-down
|
||||
a.js-delete-board-background(title="{{_ 'delete'}}")
|
||||
i.fa.fa-trash-o
|
||||
|
||||
template(name="deleteBoardBackgroundPopup")
|
||||
p {{_ 'board-background-delete-pop'}}
|
||||
button.js-confirm.negate.full(type="submit")
|
||||
i.fa.fa-trash-o
|
||||
| {{_ 'delete'}}
|
||||
|
||||
template(name="boardChangeBackgroundImagePopup")
|
||||
form
|
||||
label
|
||||
@@ -567,6 +599,10 @@ template(name="boardMenuPopup")
|
||||
a.js-change-background-image
|
||||
i.fa.fa-picture-o
|
||||
| {{_ 'board-change-background-image'}}
|
||||
li
|
||||
a.js-manage-board-backgrounds
|
||||
i.fa.fa-image
|
||||
| {{_ 'board-backgrounds'}}
|
||||
//Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
|
||||
//if currentUser.isBoardAdmin
|
||||
//
|
||||
|
||||
@@ -7,6 +7,8 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
|
||||
import AccessibilitySettings from '/models/accessibilitySettings';
|
||||
import Boards from '/models/boards';
|
||||
import Attachments from '/models/attachments';
|
||||
import { generateUniversalAttachmentUrl } from '/models/lib/universalUrlGenerator';
|
||||
import Integrations from '/models/integrations';
|
||||
import Lists from '/models/lists';
|
||||
import { BOARD_COLORS } from '/models/metadata/colors';
|
||||
@@ -347,6 +349,7 @@ Template.boardMenuPopup.events({
|
||||
},
|
||||
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
||||
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
|
||||
'click .js-manage-board-backgrounds': Popup.open('boardBackgrounds'),
|
||||
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
|
||||
'click .js-change-language': Popup.open('changeLanguage'),
|
||||
'click .js-delete-duplicate-lists': Popup.afterConfirm('deleteDuplicateLists', function() {
|
||||
@@ -869,6 +872,87 @@ Template.boardChangeBackgroundImagePopup.helpers({
|
||||
},
|
||||
});
|
||||
|
||||
// Manage the board's stored background images (upload / set active / download /
|
||||
// delete). Backgrounds are board-level Attachments (meta.boardId, no cardId,
|
||||
// meta.source === 'board-background') in the default attachments storage.
|
||||
Template.boardBackgroundsPopup.onCreated(function () {
|
||||
this.uploading = new ReactiveVar(false);
|
||||
this.error = new ReactiveVar('');
|
||||
const board = Utils.getCurrentBoard();
|
||||
this.boardId = board && board._id;
|
||||
if (this.boardId) {
|
||||
this.subscribe('boardBackgrounds', this.boardId);
|
||||
}
|
||||
});
|
||||
|
||||
Template.boardBackgroundsPopup.helpers({
|
||||
uploading() {
|
||||
return Template.instance().uploading;
|
||||
},
|
||||
error() {
|
||||
return Template.instance().error;
|
||||
},
|
||||
backgrounds() {
|
||||
// Raw collection docs don't carry the .link() helper, so compute the URL.
|
||||
return Attachments.collection
|
||||
.find({
|
||||
'meta.boardId': Template.instance().boardId,
|
||||
'meta.source': 'board-background',
|
||||
})
|
||||
.fetch()
|
||||
.map(att => ({
|
||||
_id: att._id,
|
||||
name: att.name,
|
||||
link: generateUniversalAttachmentUrl(att._id),
|
||||
}));
|
||||
},
|
||||
isActiveBackground() {
|
||||
const board = Utils.getCurrentBoard();
|
||||
return board && board.backgroundImageId === this._id;
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardBackgroundsPopup.events({
|
||||
'click .js-bg-upload-button'(event, tpl) {
|
||||
event.preventDefault();
|
||||
tpl.find('.js-bg-upload-input').click();
|
||||
},
|
||||
async 'change .js-bg-upload-input'(event, tpl) {
|
||||
const file = event.currentTarget.files && event.currentTarget.files[0];
|
||||
if (!file) return;
|
||||
tpl.error.set('');
|
||||
tpl.uploading.set(true);
|
||||
const uploader = await Attachments.insertAsync(
|
||||
{
|
||||
file,
|
||||
chunkSize: 'dynamic',
|
||||
meta: { boardId: tpl.boardId, source: 'board-background' },
|
||||
},
|
||||
false,
|
||||
);
|
||||
uploader.on('end', (err) => {
|
||||
tpl.uploading.set(false);
|
||||
if (err) tpl.error.set(err.reason || 'upload-failed');
|
||||
});
|
||||
uploader.on('error', (err) => {
|
||||
tpl.uploading.set(false);
|
||||
tpl.error.set((err && err.reason) || 'upload-failed');
|
||||
});
|
||||
uploader.start();
|
||||
// allow re-selecting the same file later
|
||||
event.currentTarget.value = '';
|
||||
},
|
||||
async 'click .js-set-board-background'() {
|
||||
const board = Utils.getCurrentBoard();
|
||||
await board.setBackgroundImage(this._id);
|
||||
Utils.setBackgroundImage();
|
||||
},
|
||||
'click .js-delete-board-background': Popup.afterConfirm('deleteBoardBackground', function () {
|
||||
Meteor.call('removeBoardBackground', this._id);
|
||||
Popup.back();
|
||||
}),
|
||||
});
|
||||
|
||||
Template.boardInfoOnMyBoardsPopup.onCreated(function() {
|
||||
this.currentBoard = Utils.getCurrentBoard();
|
||||
});
|
||||
|
||||
@@ -139,6 +139,15 @@ Template.createCustomFieldPopup.helpers({
|
||||
});
|
||||
},
|
||||
|
||||
// The template iterates `{{#each dropdownItems.get}}`. This component used to
|
||||
// be a BlazeComponent, where `dropdownItems` resolved to the instance's
|
||||
// ReactiveVar; after the migration to a plain Template that binding rendered
|
||||
// nothing (so dropdown options never appeared and were lost on save). Expose
|
||||
// the ReactiveVar as a helper again so `dropdownItems.get` works.
|
||||
dropdownItems() {
|
||||
return Template.instance().dropdownItems;
|
||||
},
|
||||
|
||||
getDropdownItems() {
|
||||
return getDropdownItems(Template.instance());
|
||||
},
|
||||
|
||||
@@ -163,6 +163,19 @@ function currentListIsInThisSwimlane(swimlaneId) {
|
||||
|
||||
function currentCardIsInThisList(listId, swimlaneId) {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
if (!currentCard) return false;
|
||||
// On desktop a clicked card is shown as a draggable popup via the openCards
|
||||
// list (rendered once in boardBody). Don't ALSO render it inline here, or the
|
||||
// card opens twice — the inline copy sits under the popup and becomes visible
|
||||
// when the popup is dragged away by its titlebar handle. Direct card-URL
|
||||
// navigation sets only currentCard (not openCards), so those still render
|
||||
// inline as before.
|
||||
if (!Utils.isMiniScreen()) {
|
||||
const openCards = Session.get('openCards') || [];
|
||||
if (openCards.includes(currentCard._id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (
|
||||
//currentUser &&
|
||||
|
||||
@@ -3,6 +3,7 @@ import '/client/components/cards/cardCustomFields.jade';
|
||||
import '/client/components/cards/cardDate.jade';
|
||||
import '/client/components/cards/cardDescription.jade';
|
||||
import '/client/components/cards/cardDetails.jade';
|
||||
import '/client/components/cards/cardStickers.jade';
|
||||
import '/client/components/cards/cardTime.jade';
|
||||
import '/client/components/cards/checklists.jade';
|
||||
import '/client/components/cards/inlinedCardDescription.jade';
|
||||
@@ -16,6 +17,7 @@ import '/client/components/cards/cardCustomFields.js';
|
||||
import '/client/components/cards/cardDate.js';
|
||||
import '/client/components/cards/cardDescription.js';
|
||||
import '/client/components/cards/cardDetails.js';
|
||||
import '/client/components/cards/cardStickers.js';
|
||||
import '/client/components/cards/cardTime.js';
|
||||
import '/client/components/cards/checklists.js';
|
||||
import '/client/components/cards/labels.js';
|
||||
@@ -31,6 +33,6 @@ import '/client/components/cards/cardTime.css';
|
||||
import '/client/components/cards/checklists.css';
|
||||
import '/client/components/cards/labels.css';
|
||||
import '/client/components/cards/minicard.css';
|
||||
import '/client/components/cards/minicardOverrides.css';
|
||||
import '/client/components/cards/resultCard.css';
|
||||
import '/client/components/cards/subtasks.css';
|
||||
|
||||
+17
-2
@@ -84,7 +84,7 @@ window.Popup = new (class {
|
||||
hasPopupParent: clickFromPopup(evt),
|
||||
title: self._getTitle(popupName),
|
||||
depth: self._stack.length,
|
||||
offset: self._getOffset(openerElement),
|
||||
offset: self._getOffset(openerElement, popupName),
|
||||
dataContext: (this && this.currentData && this.currentData()) || (options && options.dataContextIfCurrentDataIsUndefined) || this,
|
||||
});
|
||||
|
||||
@@ -217,7 +217,7 @@ window.Popup = new (class {
|
||||
// We automatically calculate the popup offset from the reference element
|
||||
// position and dimensions. We also reactively use the window dimensions to
|
||||
// ensure that the popup is always visible on the screen.
|
||||
_getOffset(element) {
|
||||
_getOffset(element, popupName) {
|
||||
const $element = $(element);
|
||||
return () => {
|
||||
Utils.windowResizeDep.depend();
|
||||
@@ -230,6 +230,21 @@ window.Popup = new (class {
|
||||
// Calculate actual popup width based on CSS: min(380px, 55vw)
|
||||
const popupWidth = Math.min(380, viewportWidth * 0.55);
|
||||
|
||||
// Card details popup: dock it to the top of the viewport so it overlays
|
||||
// the global and board header bars (its z-index already sits above them)
|
||||
// instead of opening downward from the clicked minicard.
|
||||
if (popupName === 'cardDetailsPopup') {
|
||||
const cardWidth = Math.min(600, viewportWidth * 0.8);
|
||||
const centeredLeft = (viewportWidth - cardWidth) / 2;
|
||||
// top is forced to 0 in CSS (.pop-over[data-popup="cardDetailsPopup"])
|
||||
// so the card overlays the header bars; use the full viewport height.
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft),
|
||||
top: 0,
|
||||
maxHeight: viewportHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// If the opener element is missing (e.g., programmatic open), fallback to viewport origin
|
||||
if (!$element || $element.length === 0) {
|
||||
return {
|
||||
|
||||
+13
-3
@@ -189,9 +189,19 @@ export const Utils = {
|
||||
boardCanvas.style.height = '';
|
||||
}
|
||||
} else {
|
||||
// Desktop mode: apply zoom to entire board-wrapper as before
|
||||
boardWrapper.style.transform = `scale(${level})`;
|
||||
boardWrapper.style.transformOrigin = 'top left';
|
||||
// Desktop mode: apply zoom to entire board-wrapper as before.
|
||||
// At 100% do NOT set transform: scale(1): any transform makes the
|
||||
// board-wrapper the containing block + stacking context for its
|
||||
// position:fixed descendants (the opened/dragged card), which traps the
|
||||
// card inside the board area below the page header bars. Clearing it at
|
||||
// 100% lets the opened card overlay the top bars.
|
||||
if (level === 1) {
|
||||
boardWrapper.style.transform = '';
|
||||
boardWrapper.style.transformOrigin = '';
|
||||
} else {
|
||||
boardWrapper.style.transform = `scale(${level})`;
|
||||
boardWrapper.style.transformOrigin = 'top left';
|
||||
}
|
||||
|
||||
// If zoom is 50% or lower, make board wrapper full width like content
|
||||
if (level <= 0.5) {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# WeKan multi-tenant deployment on one Linux host
|
||||
#
|
||||
# Use this compose file when you want to run several WeKan instances
|
||||
# (one per customer / domain) on the SAME Linux host, all sharing the
|
||||
# host's kernel network namespace (network_mode: "host") and one
|
||||
# MongoDB replica set, with separate logical databases per tenant.
|
||||
#
|
||||
# This file shows the minimal extra configuration needed for the
|
||||
# `uws` DDP transport plus `changeStreams` reactivity to work
|
||||
# correctly in this topology, namely:
|
||||
#
|
||||
# - DDP_TRANSPORT=uws on every service
|
||||
# - METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling on every service
|
||||
# - METEOR_SETTINGS with a DISTINCT uws.port per service (5001, 5002, …)
|
||||
#
|
||||
# Why the distinct uws.port: the uws DDP transport runs an internal
|
||||
# WebSocket proxy server on its own TCP port (default 127.0.0.1:5001)
|
||||
# and forwards WS upgrades from the public PORT into it. When two
|
||||
# services share a kernel netns and both default to 127.0.0.1:5001,
|
||||
# the second one to start fails to bind because the listen socket is
|
||||
# opened exclusively. Give each service its own internal port via
|
||||
# METEOR_SETTINGS and they coexist cleanly.
|
||||
#
|
||||
# Reference: see docs/Platforms/FOSS/Docker/Meteor3/multitenancy.md
|
||||
# and https://docs.meteor.com/performance/ddp-transport#multitenancy
|
||||
#
|
||||
# Setup:
|
||||
# docker compose -f docker-compose-multitenancy.yml up -d
|
||||
#
|
||||
# Tear down:
|
||||
# docker compose -f docker-compose-multitenancy.yml down -v
|
||||
#
|
||||
# Adding more tenants:
|
||||
# Copy a tenant block, change the service name, PORT, ROOT_URL,
|
||||
# MONGO_URL database name, and the uws.port inside METEOR_SETTINGS
|
||||
# to a fresh value (5003, 5004, …). The MongoDB service and the
|
||||
# reverse proxy in front of WeKan don't need any change.
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Shared MongoDB 7 with replica set (required for changeStreams).
|
||||
# All tenants connect to this instance using DIFFERENT logical
|
||||
# databases (?authSource if you enable auth).
|
||||
# ---------------------------------------------------------------------
|
||||
wekandb:
|
||||
image: mongo:7
|
||||
container_name: wekan-db
|
||||
restart: always
|
||||
network_mode: "host"
|
||||
entrypoint: ["sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
mongod --storageEngine wiredTiger --wiredTigerCacheSizeGB 1 --timeZoneInfo /usr/share/zoneinfo --oplogSize 2048 --replSet rs0 --bind_ip 127.0.0.1 --port 27017 --quiet &
|
||||
until mongosh --host 127.0.0.1 --port 27017 --quiet --eval 'try { const s = rs.status(); quit(s.ok === 1 ? 0 : 1); } catch (e) { if (e.codeName === "NotYetInitialized" || /no replset config has been received|not yet initialized/.test(e.message)) { rs.initiate({_id:"rs0", members:[{_id:0, host:"127.0.0.1:27017"}]}); quit(1); } quit(1); }' >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
wait
|
||||
# Report healthy only once the replica set has an elected primary, so the
|
||||
# tenant apps (depends_on: condition: service_healthy) do not start before
|
||||
# MongoDB is ready and crash with "Topology is closed". start_period gives a
|
||||
# large database time to replay its journal after an upgrade.
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- wekan-db:/data/db
|
||||
- wekan-db-dump:/dump
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Tenant 1
|
||||
#
|
||||
# Each tenant needs three things distinct from every other tenant:
|
||||
# 1. PORT — public HTTP port (here 8081)
|
||||
# 2. MONGO_URL database name — here `wekan_tenant1`
|
||||
# 3. uws.port in METEOR_SETTINGS — here 5001
|
||||
# ---------------------------------------------------------------------
|
||||
wekan-tenant1:
|
||||
image: ghcr.io/wekan/wekan:latest
|
||||
container_name: wekan-tenant1
|
||||
restart: always
|
||||
network_mode: "host"
|
||||
depends_on:
|
||||
wekandb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- PORT=8081
|
||||
- ROOT_URL=https://tenant1.example.com
|
||||
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant1?replicaSet=rs0
|
||||
- MONGO_OPLOG_URL=mongodb://127.0.0.1:27017/local?replicaSet=rs0
|
||||
- DDP_TRANSPORT=uws
|
||||
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
|
||||
# Internal uws proxy port — MUST be distinct per service.
|
||||
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
|
||||
- WRITABLE_PATH=/data
|
||||
- WITH_API=true
|
||||
volumes:
|
||||
- wekan-tenant1-data:/data
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Tenant 2 — same template, distinct PORT / database / uws.port
|
||||
# ---------------------------------------------------------------------
|
||||
wekan-tenant2:
|
||||
image: ghcr.io/wekan/wekan:latest
|
||||
container_name: wekan-tenant2
|
||||
restart: always
|
||||
network_mode: "host"
|
||||
depends_on:
|
||||
wekandb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- PORT=8082
|
||||
- ROOT_URL=https://tenant2.example.com
|
||||
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant2?replicaSet=rs0
|
||||
- MONGO_OPLOG_URL=mongodb://127.0.0.1:27017/local?replicaSet=rs0
|
||||
- DDP_TRANSPORT=uws
|
||||
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
|
||||
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5002,"host":"127.0.0.1"}}}}
|
||||
- WRITABLE_PATH=/data
|
||||
- WITH_API=true
|
||||
volumes:
|
||||
- wekan-tenant2-data:/data
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Add more tenants by copying a block above and bumping:
|
||||
# PORT (e.g. 8083, 8084, …)
|
||||
# ROOT_URL (the tenant's public domain)
|
||||
# MONGO_URL (a new database name)
|
||||
# uws.port (5003, 5004, …)
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
volumes:
|
||||
wekan-db:
|
||||
wekan-db-dump:
|
||||
wekan-tenant1-data:
|
||||
wekan-tenant2-data:
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Verifying the uws internal listen sockets are isolated.
|
||||
#
|
||||
# From the host (or any container sharing the netns):
|
||||
#
|
||||
# cat /proc/net/tcp | awk '$4 == "0A" {print $2}' | sort | uniq -c
|
||||
#
|
||||
# You want to see ONE listener per uws port — the hex addresses
|
||||
# `0100007F:1389` (5001) and `0100007F:138A` (5002) should each
|
||||
# appear with count 1, no duplicates. If you see `2 0100007F:1389`,
|
||||
# two services are trying to share the default port; reconfigure
|
||||
# their METEOR_SETTINGS.uws.port to distinct values and restart.
|
||||
# ---------------------------------------------------------------------
|
||||
+36
-5
@@ -258,6 +258,17 @@ services:
|
||||
- wekan-tier
|
||||
expose:
|
||||
- 27017
|
||||
# Healthcheck: report healthy only once the replica set has an elected
|
||||
# primary, so the wekan app (depends_on: condition: service_healthy) does
|
||||
# not start before MongoDB is ready and crash with "Topology is closed".
|
||||
# start_period gives a large database time to replay its journal after an
|
||||
# upgrade before failures are counted.
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-db:/data/db
|
||||
@@ -315,9 +326,9 @@ services:
|
||||
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
|
||||
# Use oplog,polling to fix performance until changeStreams is fixed at next release of Meteor 3.5 Beta:
|
||||
# https://github.com/wekan/wekan/issues/6307#issuecomment-4299349231
|
||||
- METEOR_REACTIVITY_ORDER=oplog,polling
|
||||
- DDP_TRANSPORT=sockjs
|
||||
#- DDP_TRANSPORT=uws
|
||||
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
|
||||
- DDP_TRANSPORT=uws
|
||||
#- DDP_TRANSPORT=sockjs
|
||||
#-----------------------------------------------------------------
|
||||
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
|
||||
- WRITABLE_PATH=/data
|
||||
@@ -793,7 +804,15 @@ services:
|
||||
# The limit number of entries (0=unlimited)
|
||||
#- LDAP_SEARCH_SIZE_LIMIT=0
|
||||
#
|
||||
# Enable group filtering. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap.
|
||||
# Enable the login restriction group filter. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap.
|
||||
# When true, only members of LDAP_GROUP_FILTER_GROUP_NAME are allowed to log in.
|
||||
# NOTE: This flag ONLY controls the login restriction. Admin status sync
|
||||
# (LDAP_SYNC_ADMIN_STATUS / LDAP_SYNC_ADMIN_GROUPS) and group->role sync
|
||||
# (LDAP_SYNC_GROUP_ROLES) query LDAP groups independently and do NOT require
|
||||
# this flag to be true. The group filter metadata below
|
||||
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
|
||||
# must still be configured for any group search to work.
|
||||
#- LDAP_GROUP_FILTER_ENABLE=false
|
||||
#
|
||||
# The object class for filtering. Example: group
|
||||
@@ -847,6 +866,12 @@ services:
|
||||
# with the LDAP_SYNC_USER_DATA_FIELDMAP is defined in setting LDAP_DEFAULT_DOMAIN above.
|
||||
#
|
||||
# Enable/Disable syncing of admin status based on ldap groups:
|
||||
# NOTE: Admin status sync and group->role sync (LDAP_SYNC_GROUP_ROLES) query
|
||||
# LDAP groups on their own. They no longer require LDAP_GROUP_FILTER_ENABLE=true,
|
||||
# which only controls the login restriction filter. The group filter metadata
|
||||
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
|
||||
# must still be configured for the group search to work.
|
||||
#- LDAP_SYNC_ADMIN_STATUS=true
|
||||
#
|
||||
# Comma separated list of admin group names to sync.
|
||||
@@ -902,7 +927,13 @@ services:
|
||||
# Wait spinner to use
|
||||
# - WAIT_SPINNER=Bounce
|
||||
#---------------------------------------------------------------------
|
||||
#depends_on:
|
||||
# Wait for MongoDB to be healthy (replica-set primary elected) before
|
||||
# starting WeKan, so index creation does not crash with "Topology is closed".
|
||||
# If you use FerretDB instead, comment out the wekandb block below and
|
||||
# uncomment the wekan-ferretdb block.
|
||||
depends_on:
|
||||
wekandb:
|
||||
condition: service_healthy
|
||||
# wekan-migrate:
|
||||
# condition: service_completed_successfully
|
||||
# wekan-ferretdb:
|
||||
|
||||
+9
-1
@@ -378,7 +378,15 @@ services:
|
||||
# LDAP_SEARCH_SIZE_LIMIT : The limit number of entries (0=unlimited)
|
||||
# example : LDAP_SEARCH_SIZE_LIMIT=12345
|
||||
- LDAP_SEARCH_SIZE_LIMIT=0
|
||||
# LDAP_GROUP_FILTER_ENABLE : Enable group filtering
|
||||
# LDAP_GROUP_FILTER_ENABLE : Enable the login restriction group filter.
|
||||
# When true, only members of LDAP_GROUP_FILTER_GROUP_NAME are allowed to log in.
|
||||
# NOTE: This flag ONLY controls the login restriction. Admin status sync
|
||||
# (LDAP_SYNC_ADMIN_STATUS / LDAP_SYNC_ADMIN_GROUPS) and group->role sync
|
||||
# (LDAP_SYNC_GROUP_ROLES) query LDAP groups independently and do NOT require
|
||||
# this flag to be true. The group filter metadata below
|
||||
# (LDAP_GROUP_FILTER_OBJECTCLASS, LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE,
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT, LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE)
|
||||
# must still be configured for any group search to work.
|
||||
# example : LDAP_GROUP_FILTER_ENABLE=true
|
||||
- LDAP_GROUP_FILTER_ENABLE=false
|
||||
# LDAP_GROUP_FILTER_OBJECTCLASS : The object class for filtering
|
||||
|
||||
@@ -31,14 +31,13 @@
|
||||
# - WeKan hosting CloudFlare DNS: customer.wekan.team A Server-IPv4-Address
|
||||
|
||||
wekan.customer.com {
|
||||
# b) No custom domain, using customer.wekan.team:
|
||||
# - Using CloudFlare TLS Origin Certificate with Strict Enforcement
|
||||
# - At CloudFlare DNS change to orange cloud, with CloudFlare proxy,
|
||||
# - https://github.com/wekan/wekan/blob/main/docs/Webserver/Caddy.md
|
||||
# - Add this tls section to this example:
|
||||
# tls {
|
||||
# load /etc/caddy/certs
|
||||
# }
|
||||
# b) No custom domain, using customer.wekan.team:
|
||||
# - Using CloudFlare TLS Origin Certificate with Strict Enforcement
|
||||
# - At CloudFlare DNS change to orange cloud, with CloudFlare proxy,
|
||||
# - https://github.com/wekan/wekan/blob/main/docs/Webserver/Caddy.md
|
||||
# tls {
|
||||
# load /etc/caddy/certs
|
||||
# }
|
||||
header {
|
||||
-Content-Security-Policy-Report-Only
|
||||
-Content-Security-Policy
|
||||
@@ -54,7 +53,7 @@ wekan.customer.com {
|
||||
header Access-Control-Allow-Origin *
|
||||
}
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:3039
|
||||
reverse_proxy 127.0.0.1:3001
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,16 +64,16 @@ wekan.customer.com {
|
||||
# - Difference is additional tls section: tls { load /etc/caddy/certs }
|
||||
|
||||
customer.wekan.team {
|
||||
tls {
|
||||
load /etc/caddy/certs
|
||||
}
|
||||
tls {
|
||||
load /etc/caddy/certs
|
||||
}
|
||||
header {
|
||||
-Content-Security-Policy-Report-Only
|
||||
-Content-Security-Policy
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://static.cloudflareinsights.com https://accounts.google.com https://apis.google.com; worker-src 'self' blob:; connect-src 'self' wss://customer.wekan.team https://customer.wekan.team https://cloudflareinsights.com https://accounts.google.com; img-src 'self' data: https://*.googleusercontent.com; frame-src 'self' https://accounts.google.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:;"
|
||||
Cross-Origin-Opener-Policy "same-origin-allow-popups"
|
||||
X-Content-Type-Options nosniff
|
||||
Strict-Transport-Security "max-age=31536000;"
|
||||
Strict-Transport-Security "max-age=31536000;"
|
||||
-X-Frame-Options
|
||||
Cache-Control "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
@@ -82,9 +81,10 @@ customer.wekan.team {
|
||||
header Content-Type application/json
|
||||
header Access-Control-Allow-Origin *
|
||||
}
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:3040
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:3002
|
||||
}
|
||||
}
|
||||
|
||||
# 3) Static website
|
||||
@@ -247,6 +247,7 @@ boards.wekan.team {
|
||||
tls {
|
||||
load /etc/caddy/certs
|
||||
}
|
||||
|
||||
header {
|
||||
-Content-Security-Policy-Report-Only
|
||||
-Content-Security-Policy
|
||||
@@ -433,8 +434,8 @@ boards.wekan.team {
|
||||
"prefer_related_applications": false
|
||||
}`
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:3025
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +1,96 @@
|
||||
# Multitenancy
|
||||
|
||||
TLDR a) and b)
|
||||
Run several WeKan instances (one per customer / domain) on the same
|
||||
Linux host. Two supported topologies, pick whichever fits your
|
||||
deployment.
|
||||
|
||||
## a) Oplog sockjs
|
||||
## a) Recommended — one host, many WeKan domains, `uws` + `changeStreams`
|
||||
|
||||
- One server/Many WeKan domains/One IPv4
|
||||
- Multitenancy works
|
||||
Highest DDP throughput and lowest reactive latency.
|
||||
|
||||
docker-compose.yml:
|
||||
Configure each instance with:
|
||||
|
||||
- `DDP_TRANSPORT=uws`
|
||||
- `METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling`
|
||||
- A **distinct** `Meteor.settings.packages['ddp-server'].uws.port`
|
||||
per instance (`5001`, `5002`, `5003`, … in the example below)
|
||||
- `network_mode: "host"` (or any setup that shares one kernel
|
||||
network namespace)
|
||||
- The same MongoDB replica set (separate logical databases per
|
||||
tenant)
|
||||
|
||||
Why the distinct `uws.port`: the `uws` DDP transport runs its own
|
||||
internal WebSocket proxy server on a dedicated TCP port (default
|
||||
`127.0.0.1:5001`) and forwards public WebSocket upgrades into it.
|
||||
The internal listen socket is opened exclusively, so each instance
|
||||
in the same kernel netns must declare its own internal port.
|
||||
|
||||
Working example: see [`docker-compose-multitenancy.yml`](../../../../../docker-compose-multitenancy.yml)
|
||||
in the repository root.
|
||||
|
||||
Sketch of the per-tenant environment block:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- PORT=8081 # public HTTP port
|
||||
- ROOT_URL=https://tenant1.example.com
|
||||
- MONGO_URL=mongodb://127.0.0.1:27017/wekan_tenant1?replicaSet=rs0
|
||||
- DDP_TRANSPORT=uws
|
||||
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
|
||||
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
|
||||
```
|
||||
|
||||
For tenant 2, bump `PORT`, `ROOT_URL`, the MongoDB database name, and
|
||||
the `uws.port` (e.g. `5002`). The reverse proxy in front of WeKan
|
||||
keeps routing customer domains to each tenant's public `PORT`
|
||||
exactly as before — the internal `uws.port` is never exposed.
|
||||
|
||||
### Verifying the configuration
|
||||
|
||||
From the host (or from inside any container sharing the netns):
|
||||
|
||||
```sh
|
||||
cat /proc/net/tcp | awk '$4 == "0A" {print $2}' | sort | uniq -c
|
||||
```
|
||||
|
||||
Each `uws.port` should appear exactly once. `0x1389` is `5001`,
|
||||
`0x138A` is `5002`, etc. If you see `2 0100007F:1389`, two tenants
|
||||
are trying to share the default port; reconfigure their
|
||||
`METEOR_SETTINGS.uws.port` to distinct values and restart.
|
||||
|
||||
### If you forget to set a distinct `uws.port`
|
||||
|
||||
The second tenant to start refuses the bind and Meteor throws at
|
||||
startup with an actionable error:
|
||||
|
||||
```text
|
||||
Error: uWebSockets.js: failed to listen on 127.0.0.1:5001 (address already in use).
|
||||
Another Meteor instance in this network namespace is already bound to this port.
|
||||
Set a distinct Meteor.settings.packages["ddp-server"].uws.port (or .host) for each instance.
|
||||
```
|
||||
|
||||
Add a `METEOR_SETTINGS` line with a free port to the failing
|
||||
service, restart.
|
||||
|
||||
## b) Alternative — `sockjs` + `oplog` (maximum proxy compatibility)
|
||||
|
||||
Use this if your reverse proxy / load balancer cannot guarantee
|
||||
WebSocket upgrade pass-through, or if some clients sit on networks
|
||||
that block raw WebSocket. SockJS will fall back to HTTP long-polling
|
||||
on those clients.
|
||||
|
||||
```yaml
|
||||
- DDP_TRANSPORT=sockjs
|
||||
- METEOR_REACTIVITY_ORDER=oplog,polling
|
||||
- DDP_TRANSPORT=uws
|
||||
```
|
||||
|
||||
## b) changeStreams uws
|
||||
No per-tenant `uws.port` configuration is needed because `sockjs`
|
||||
does not run a separate internal listen socket. The trade-off is
|
||||
lower DDP throughput compared to `uws`.
|
||||
|
||||
- One server/One WeKan domain/One IPv4
|
||||
## Reference
|
||||
|
||||
Multitenancy does not work, causes bug: round robin changing
|
||||
WeKan website at every webpage reload at same address.
|
||||
|
||||
For example:
|
||||
|
||||
1) At https://boards.wekan.team shows https://wekan.customer2.com
|
||||
2) Reload webpage. https://boards.wekan.team shows https://wekan.customer3.com
|
||||
3) Reload webpage. https://boards.wekan.team shows https://wekan.customer4.com
|
||||
- 4) Reload webpage. https://boards.wekan.team shows https://customer4.wekan.team
|
||||
- etc.
|
||||
|
||||
## Details
|
||||
|
||||
When multiple Meteor 3 applications are running on the same server using
|
||||
`network_mode: "host"` and are shared across the same MongoDB server,
|
||||
the combination `DDP_TRANSPORT=uws` and `METEOR_REACTIVITY_ORDER=changeStreams`
|
||||
is subject to a very rare but critical **Context/Descriptor Confusion**.
|
||||
|
||||
Here is a detailed technical explanation of what is happening behind the scenes and why it breaks a multi-tenant environment:
|
||||
|
||||
### 1. µWebSockets (`uws`) and shared host networking
|
||||
|
||||
`uws` is an extremely high-performance WebSocket engine written in C++.
|
||||
It bypasses many of the traditional networking layers of Node.js and
|
||||
handles network sockets and their file handles (File Descriptors)
|
||||
directly at a low level in its own Event Loop.
|
||||
|
||||
When containers are set to `network_mode: "host"`, they are not
|
||||
network-level isolated. They share the same Linux kernel
|
||||
network stack and memory space for network traffic.
|
||||
When your browser does a WebSocket handshake (`Upgrade: websocket`)
|
||||
after an HTTP request, `uws` allocates a memory address for the socket.
|
||||
|
||||
The bug occurs when multiple containers do this at the same time
|
||||
in the same host network environment,
|
||||
**the low-level C++ code in the `uws` engine can cross-identify socket identifiers from different processes**
|
||||
if the framework (in this case, an early beta integration of Meteor 3)
|
||||
is unable to keep connection ownership (v8 isolation) completely separate.
|
||||
|
||||
### 2. `changeStreams` and data misrouting
|
||||
|
||||
MongoDB's `changeStreams` relies on the application opening a long-lived,
|
||||
real-time listening pipe to the database. It is heavier and more complex
|
||||
than the old `oplog` table because it creates constantly changing,
|
||||
stateful cursors between the application and the database.
|
||||
|
||||
When this is combined with `uws` WebSocket:
|
||||
|
||||
1. User A opens the `boards.wekan.team` page.
|
||||
|
||||
2. The browser requests a WebSocket connection, and Meteor creates a
|
||||
`changeStream` listener on the database for User A's container.
|
||||
|
||||
3. At the same time, User B loads the `kanban.buetzow.de` page.
|
||||
|
||||
4. Since both containers talk locally to the same MongoDB process
|
||||
and share the same host interface, the application layer's
|
||||
reactivity engine (multiplexer) confuses which `changeStream`
|
||||
data input belongs to which active `uws` socket address.
|
||||
|
||||
**Result:** A reactive data packet from the database (e.g. different customer 1 boards)
|
||||
is pushed into the `uws` pipe that was locally open to the `boards.wekan.team` browser.
|
||||
The browser gets a perfectly valid HTML body, but the Javascript layer draws the data
|
||||
it received from the WebSocket.
|
||||
|
||||
### Why did good old `sockjs` and `oplog` fix the situation?
|
||||
|
||||
* **`sockjs`** is written in pure JavaScript (Node.js' native network stack).
|
||||
It strictly follows Node.js' own process boundaries, and does not perform
|
||||
direct C++-level optimizations beyond the kernel. Even though containers
|
||||
share the host network, Node.js makes sure that each process's sockets
|
||||
remain strictly within its own application window.
|
||||
|
||||
* **`oplog`** is a more passive way to read the database. The application
|
||||
reads the raw log file (`local.oplog.rs`) and filters its own data from
|
||||
there, instead of MongoDB's active `changeStream` engine trying to route
|
||||
and load balance cursors between different processes on the fly.
|
||||
|
||||
Simply put: `uws` and `changeStreams` are great tools when you are running
|
||||
**only one large application** on a single server for which you want
|
||||
maximum performance. In a multi-tenant environment
|
||||
(many different clients on the same machine), they are too aggressive
|
||||
and break the invisible boundaries between containers.
|
||||
- Meteor 3.5 DDP transport docs, including the multi-tenant section:
|
||||
<https://docs.meteor.com/performance/ddp-transport#multitenancy>
|
||||
- Meteor changeStreams reactivity driver:
|
||||
<https://docs.meteor.com/performance/change-streams-observer-driver>
|
||||
@@ -17,7 +17,7 @@ for d in */ ; do
|
||||
docker compose up -d --force-recreate
|
||||
#docker compose stop
|
||||
#docker compose start
|
||||
#docker rm "${FOLDER_NAME}-app"
|
||||
#docker rm "${FOLDER_NAME}-tenant"
|
||||
#docker compose up -d
|
||||
|
||||
# Return to the parent directory
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
# ========== CUSTOM END =====
|
||||
## See ../multitenancy.md about commented out settings:
|
||||
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
|
||||
- DDP_TRANSPORT=uws
|
||||
#- METEOR_REACTIVITY_ORDER=oplog,polling
|
||||
#- DDP_TRANSPORT=sockjs
|
||||
- WRITABLE_PATH=/data
|
||||
- WITH_API=true
|
||||
- RICHER_CARD_COMMENT_EDITOR=false
|
||||
- CARD_OPENED_WEBHOOK_ENABLED=false
|
||||
- BIGEVENTS_PATTERN=NONE
|
||||
- NODE_FLAGS=--max-old-space-size=16384
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
- MONGO_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/els?authSource=els
|
||||
- MONGO_OPLOG_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/local?replicaSet=rs0&authSource=els
|
||||
- PORT=3001
|
||||
- ROOT_URL=https://wekan.customer.com
|
||||
- MONGO_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/PARAMETER?replicaSet=rs0&appName=PARAMETER-tenant&authSource=PARAMETER
|
||||
- MONGO_OPLOG_URL=mongodb://PARAMETER_user:PASSWORD@localhost:27017/local?replicaSet=rs0&appName=PARAMETER-tenant-oplog&authSource=PARAMETER
|
||||
# Distinct uws internal port per tenant — required since the
|
||||
# 3.5-beta.12 fix (PR #14425) binds with LIBUS_LISTEN_EXCLUSIVE_PORT.
|
||||
- METEOR_SETTINGS={"packages":{"ddp-server":{"uws":{"port":5001,"host":"127.0.0.1"}}}}
|
||||
- MAIL_FROM=Customer WeKan Boards <boards@wekan.team>
|
||||
- MAIL_URL=smtp://AKIA-USERNAME:PASSWORD@email-smtp.eu-west-1.amazonaws.com:587?tls={ciphers:"SSLv3"}&secureConnection=false
|
||||
- PORT=3032
|
||||
- ROOT_URL=https://wekan.customer.com
|
||||
@@ -2,19 +2,8 @@ services:
|
||||
|
||||
PARAMETER:
|
||||
image: ghcr.io/wekan/wekan:v9.20
|
||||
container_name: PARAMETER-app
|
||||
restart: always
|
||||
container_name: PARAMETER-tenant
|
||||
restart: unless-stopped
|
||||
network_mode: "host"
|
||||
environment:
|
||||
## See ../multitenancy.md about commented out settings:
|
||||
#- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
|
||||
- METEOR_REACTIVITY_ORDER=oplog,polling
|
||||
#- DDP_TRANSPORT=uws
|
||||
- DDP_TRANSPORT=sockjs
|
||||
- WRITABLE_PATH=/data
|
||||
- WITH_API=true
|
||||
- RICHER_CARD_COMMENT_EDITOR=false
|
||||
- CARD_OPENED_WEBHOOK_ENABLED=false
|
||||
- BIGEVENTS_PATTERN=NONE
|
||||
- NODE_FLAGS=--max-old-space-size=16384
|
||||
# ========== CUSTOM START =====
|
||||
@@ -14,7 +14,7 @@ for d in restore/*/ ; do
|
||||
|
||||
# Run the Docker commands using the folder name
|
||||
docker compose down
|
||||
docker rm "${FOLDER_NAME}-app"
|
||||
docker rm "${FOLDER_NAME}-tenant"
|
||||
docker compose up -d --force-recreate
|
||||
|
||||
# Return to the parent directory
|
||||
|
||||
@@ -25,7 +25,7 @@ for d in restore/*/ ; do
|
||||
|
||||
# Run the Docker commands using the clean folder name
|
||||
docker compose down
|
||||
docker rm "${FOLDER_NAME}-app" 2>/dev/null # 2>/dev/null ignores errors if it's already gone
|
||||
docker rm "${FOLDER_NAME}-tenant" 2>/dev/null # 2>/dev/null ignores errors if it's already gone
|
||||
docker compose up -d --force-recreate
|
||||
)
|
||||
done
|
||||
|
||||
@@ -10,19 +10,19 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-9.33-win64.zip](https://github.com/wekan/wekan/releases/download/v9.33/wekan-9.33-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
1. [wekan-9.42-win64.zip](https://github.com/wekan/wekan/releases/download/v9.42/wekan-9.42-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
|
||||
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
|
||||
|
||||
3. [mongodb-windows-x86_64-7.0.32-signed.msi](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.32-signed.msi)
|
||||
3. [mongodb-windows-x86_64-7.0.34-signed.msi](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.34-signed.msi)
|
||||
|
||||
4. (already exists: [start-wekan.bat](https://raw.githubusercontent.com/wekan/wekan/main/start-wekan.bat))
|
||||
|
||||
5. Copy files from steps 1-3 with USB stick or DVD to offline Windows computer
|
||||
|
||||
6. Double click `mongodb-windows-x86_64-7.0.32-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
||||
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
||||
|
||||
7. Unzip `wekan-9.33-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
7. Unzip `wekan-9.42-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
|
||||
```
|
||||
bundle (directory)
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Tempelkan data JSON yang sah disini",
|
||||
"import-csv-placeholder": "Tampal data CSV/TSV yang sah di sini",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Petakan partisipan",
|
||||
"import-members-map": "Papan yang diimport mempunyai ahli. Sila padankan ahli yang anda mahukan untuk import kepada pengguna anda",
|
||||
"import-members-map-note": "Ahli yang tidak dipadan akan di padankan kepada pengguna yang ada",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Urutan aktif",
|
||||
"cardsSortPopup-title": "Susun Kad",
|
||||
"due-date": "Tarikh Akhir",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Ralat Komputer Pelayan",
|
||||
"server-error-troubleshooting": "Sila masukkan ralat yang diberikan oleh komputer pelayan. \nUntuk instalasi menggunakan snap, larikan: 'sudo snap logs wekan.wekan'\nUntuk Instalasi menggunakan Docker, larikan: 'sudo docker logs wekan-app'",
|
||||
"title-alphabetically": "Judul (mengikut abjad)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Daftar Lampiran",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "tipe",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "As jy foute kry wanneer jy bord invoer, werk die invoer soms steeds, en bord is op Alle Borde-bladsy.",
|
||||
"import-json-placeholder": "Plak jou geldige JSON-data hier",
|
||||
"import-csv-placeholder": "Plak jou geldige CSV/TSV-data hier",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Kaart lede",
|
||||
"import-members-map": "Jou ingevoerde raad het 'n paar lede. Karteer asseblief die lede wat jy wil invoer na jou gebruikers toe",
|
||||
"import-members-map-note": "Let wel: Ongekarte lede sal aan die huidige gebruiker toegewys word.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sorteer is aan",
|
||||
"cardsSortPopup-title": "Sorteer kaarte",
|
||||
"due-date": "Sperdatum",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Bedienerfout",
|
||||
"server-error-troubleshooting": "Dien asseblief die fout in wat deur die bediener gegenereer is.\nVir 'n vinnige installasie, hardloop: `sudo snap logs wekan.wekan`\nVir 'n Docker-installasie, hardloop: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Titel (alfabeties)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Aanhangsels",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Tik",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Kon nie migrasies stop nie",
|
||||
"migration-stopped": "Migrasies is suksesvol gestop",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS-berging",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Onderbreek alle migrasies",
|
||||
"s3-access-key": "S3 Toegangssleutel",
|
||||
"s3-access-key-description": "AWS S3 toegangsleutel vir verifikasie",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "As jy foute kry wanneer jy bord invoer, werk die invoer soms steeds, en bord is op Alle Borde-bladsy.",
|
||||
"import-json-placeholder": "Plak jou geldige JSON-data hier",
|
||||
"import-csv-placeholder": "Plak jou geldige CSV/TSV-data hier",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Kaart lede",
|
||||
"import-members-map": "Jou ingevoerde raad het 'n paar lede. Karteer asseblief die lede wat jy wil invoer na jou gebruikers toe",
|
||||
"import-members-map-note": "Let wel: Ongekarte lede sal aan die huidige gebruiker toegewys word.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sorteer is aan",
|
||||
"cardsSortPopup-title": "Sorteer kaarte",
|
||||
"due-date": "Sperdatum",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Bedienerfout",
|
||||
"server-error-troubleshooting": "Dien asseblief die fout in wat deur die bediener gegenereer is.\nVir 'n vinnige installasie, hardloop: `sudo snap logs wekan.wekan`\nVir 'n Docker-installasie, hardloop: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Titel (alfabeties)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Aanhangsels",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Tik",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Kon nie migrasies stop nie",
|
||||
"migration-stopped": "Migrasies is suksesvol gestop",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS-berging",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Onderbreek alle migrasies",
|
||||
"s3-access-key": "S3 Toegangssleutel",
|
||||
"s3-access-key-description": "AWS S3 toegangsleutel vir verifikasie",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
|
||||
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
|
||||
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "رسم خريطة الأعضاء",
|
||||
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
|
||||
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "الفرز قيد التشغيل",
|
||||
"cardsSortPopup-title": "مرتبسازی کارتها",
|
||||
"due-date": "تاریخ اجرا",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "خطأ بالخادم",
|
||||
"server-error-troubleshooting": "Merci de soumettre l'erreur générée par le serveur. Pour une installation snap, lancer `sudo snap logs wekan.wekan`. Pour une installation docker, lancer `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "تیتر (به صورت الفبا)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "المرفقات",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "النوع",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
|
||||
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
|
||||
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "رسم خريطة الأعضاء",
|
||||
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
|
||||
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "الفرز قيد التشغيل",
|
||||
"cardsSortPopup-title": "مرتبسازی کارتها",
|
||||
"due-date": "تاریخ اجرا",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "خطأ بالخادم",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "تیتر (به صورت الفبا)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "المرفقات",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "النوع",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
|
||||
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
|
||||
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "رسم خريطة الأعضاء",
|
||||
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
|
||||
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "الفرز قيد التشغيل",
|
||||
"cardsSortPopup-title": "مرتبسازی کارتها",
|
||||
"due-date": "تاریخ اجرا",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "خطأ بالخادم",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "تیتر (به صورت الفبا)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "المرفقات",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "النوع",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "اگر هنگام بازگردانی با خطا مواجه شدید بعضی اوقات بازگردانی انجام می شود و تمامی برد ها در داخل صفحه All Boards هستند",
|
||||
"import-json-placeholder": "اطلاعات Json معتبر خود را اینجا وارد کنید.",
|
||||
"import-csv-placeholder": "اطلاعات CSV/TSV خود را اینجا پیست کنید.",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "رسم خريطة الأعضاء",
|
||||
"import-members-map": "برد ها بازگردانده شده تعدادی کاربر دارند . لطفا کاربر های که می خواهید را انتخاب نمایید",
|
||||
"import-members-map-note": "نکته: اعضا بدون نقشه به کاربر فعلی واگذار می شوند",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "الفرز قيد التشغيل",
|
||||
"cardsSortPopup-title": "مرتبسازی کارتها",
|
||||
"due-date": "تاریخ اجرا",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "خطأ بالخادم",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "تیتر (به صورت الفبا)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "المرفقات",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "النوع",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Aunque obtengas errores cuando importes el tablero, a veces la importación funciona igualmente, y el tablero se encontrará en la página de tableros.",
|
||||
"import-json-placeholder": "Pega tus datos JSON válidos aquí",
|
||||
"import-csv-placeholder": "Pega tus datos CSV/TSV válidos aquí",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mapa de miembros",
|
||||
"import-members-map": "Tu tablero importado tiene algunos miembros. Por favor, mapea los miembros que quieres importar con tus usuarios.",
|
||||
"import-members-map-note": "Nota: Los miembros no mapeados serán asignados al usuario actual.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Ordenamiento activado",
|
||||
"cardsSortPopup-title": "Ordenar tarjetas",
|
||||
"due-date": "Fecha de Vencimiento",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Error del Servidor",
|
||||
"server-error-troubleshooting": "Por favor, envíe el error generado por el servidor.\nPara la instalación con snap, ejecute: `sudo snap logs wekan.wekan`\nPara la instalación con docker, ejecute: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Título (Alfabéticamente)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Adjuntos",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Tipo",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Fallo al detener las migraciones",
|
||||
"migration-stopped": "Las migraciones se han detenido correctamente",
|
||||
"mongodb-gridfs-storage": "Almacenamiento MongoDB GridFS",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pausar todas las migraciones",
|
||||
"s3-access-key": "Clave de acceso de S3",
|
||||
"s3-access-key-description": "Clave de acceso de AWS S3 para autentificación",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Lövhəni idxal edərkən səhvlər alsanız, bəzən idxal hələ də işləyir və lövhə Bütün Lövhələr səhifəsindədir.",
|
||||
"import-json-placeholder": "Etibarlı JSON datanızı bura yerləşdirin",
|
||||
"import-csv-placeholder": "Etibarlı CSV/TSV datanızı bura yerləşdirin",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Xəritə üzvləri",
|
||||
"import-members-map": "İdxal etdiyiniz şuranın bəzi üzvləri var. Xahiş edirik, idxal etmək istədiyiniz üzvləri istifadəçilərinizə xəritələşdirin",
|
||||
"import-members-map-note": "Qeyd: Xəritəsiz üzvlər cari istifadəçiyə təyin ediləcək.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort aktivdir",
|
||||
"cardsSortPopup-title": "Kartları çeşidləyin",
|
||||
"due-date": "Son tarix",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server xətası",
|
||||
"server-error-troubleshooting": "Zəhmət olmasa server tərəfindən yaradılan xətanı təqdim edin.\nSürətli quraşdırma üçün işləyin: `sudo snap logs wekan.wekan`\nDocker quraşdırması üçün işləyin: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Başlıq (Əlifba sırası ilə)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Qoşmalar",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Növ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Miqrasiyanı dayandırmaq alınmadı",
|
||||
"migration-stopped": "Miqrasiya uğurla dayandırıldı",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Saxlama",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Bütün miqrasiyaları dayandırın",
|
||||
"s3-access-key": "S3 Giriş Açarı",
|
||||
"s3-access-key-description": "Doğrulama üçün AWS S3 giriş açarı",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Lövhəni idxal edərkən səhvlər alsanız, bəzən idxal hələ də işləyir və lövhə Bütün Lövhələr səhifəsindədir.",
|
||||
"import-json-placeholder": "Etibarlı JSON datanızı bura yerləşdirin",
|
||||
"import-csv-placeholder": "Etibarlı CSV/TSV datanızı bura yerləşdirin",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Xəritə üzvləri",
|
||||
"import-members-map": "İdxal etdiyiniz şuranın bəzi üzvləri var. Xahiş edirik, idxal etmək istədiyiniz üzvləri istifadəçilərinizə xəritələşdirin",
|
||||
"import-members-map-note": "Qeyd: Xəritəsiz üzvlər cari istifadəçiyə təyin ediləcək.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort aktivdir",
|
||||
"cardsSortPopup-title": "Kartları çeşidləyin",
|
||||
"due-date": "Son tarix",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server xətası",
|
||||
"server-error-troubleshooting": "Zəhmət olmasa server tərəfindən yaradılan xətanı təqdim edin.\nSürətli quraşdırma üçün işləyin: `sudo snap logs wekan.wekan`\nDocker quraşdırması üçün işləyin: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Başlıq (Əlifba sırası ilə)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Qoşmalar",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Növ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Miqrasiyanı dayandırmaq alınmadı",
|
||||
"migration-stopped": "Miqrasiya uğurla dayandırıldı",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Saxlama",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Bütün miqrasiyaları dayandırın",
|
||||
"s3-access-key": "S3 Giriş Açarı",
|
||||
"s3-access-key-description": "Doğrulama üçün AWS S3 giriş açarı",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Lövhəni idxal edərkən səhvlər alsanız, bəzən idxal hələ də işləyir və lövhə Bütün Lövhələr səhifəsindədir.",
|
||||
"import-json-placeholder": "Etibarlı JSON datanızı bura yerləşdirin",
|
||||
"import-csv-placeholder": "Etibarlı CSV/TSV datanızı bura yerləşdirin",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Xəritə üzvləri",
|
||||
"import-members-map": "İdxal etdiyiniz şuranın bəzi üzvləri var. Xahiş edirik, idxal etmək istədiyiniz üzvləri istifadəçilərinizə xəritələşdirin",
|
||||
"import-members-map-note": "Qeyd: Xəritəsiz üzvlər cari istifadəçiyə təyin ediləcək.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort aktivdir",
|
||||
"cardsSortPopup-title": "Kartları çeşidləyin",
|
||||
"due-date": "Son tarix",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server xətası",
|
||||
"server-error-troubleshooting": "Zəhmət olmasa server tərəfindən yaradılan xətanı təqdim edin.\nSürətli quraşdırma üçün işləyin: `sudo snap logs wekan.wekan`\nDocker quraşdırması üçün işləyin: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Başlıq (Əlifba sırası ilə)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Qoşmalar",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Növ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Miqrasiyanı dayandırmaq alınmadı",
|
||||
"migration-stopped": "Miqrasiya uğurla dayandırıldı",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Saxlama",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Bütün miqrasiyaları dayandırın",
|
||||
"s3-access-key": "S3 Giriş Açarı",
|
||||
"s3-access-key-description": "Doğrulama üçün AWS S3 giriş açarı",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Ако се јаве грешке при уношењу списа то не значи да унос није успео!Проверите јел се спис појавио у одељку „Списи у мојој надлежности“.",
|
||||
"import-json-placeholder": "Копирайте валидната Ви JSON информация тук",
|
||||
"import-csv-placeholder": "Овде сместите Ваше исправне CSV/TSV податке",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Мапирај сараднике",
|
||||
"import-members-map": "Списи које сте унели имају постављену мрежу сарадника. Молим да мапирате сараднике из те мреже на Ваше сараднике",
|
||||
"import-members-map-note": "Белешка: Немапирани сарадници ће бити додељени на тренутног корисника.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Предмети су сложени",
|
||||
"cardsSortPopup-title": "Сложи предмете",
|
||||
"due-date": "По крајњем року",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Грешка на серверу",
|
||||
"server-error-troubleshooting": "Молим да нам пошаљете извештај о грешци коју је изазвао сервер.\nАко је у питању snap инсталација, покрените: `sudo snap logs wekan.wekan`\nАко је у питању Docker инсталација, покрените: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "По наслову абучним редом",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Прикачени файлове",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Тип",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Није било могуће прекинути поступке обнове оштећених списа",
|
||||
"migration-stopped": "Поступци обнове оштећених списа су управо заустављени",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS складиште",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Предах за све",
|
||||
"s3-access-key": "S3 приступни кључ",
|
||||
"s3-access-key-description": "AWS S3 приступни кључ за пријаву",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Si une erreur survient en important le tableau, il se peut que l'import ait fonctionné, et que le tableau se trouve sur la page \"Tous les tableaux\".",
|
||||
"import-json-placeholder": "Collez ici les données JSON valides",
|
||||
"import-csv-placeholder": "Déposez ici vos données valides CSV/TSV",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Assigner des participants",
|
||||
"import-members-map": "Le tableau que vous venez d'importer contient des participants. Veuillez assigner les participants que vous souhaitez importer à vos utilisateurs.",
|
||||
"import-members-map-note": "Note: les participants ne concordant pas seront assignés à l'utilisateur courant.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Le tri est activé",
|
||||
"cardsSortPopup-title": "Trier les cartes",
|
||||
"due-date": "Date d'échéance",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Erreur serveur",
|
||||
"server-error-troubleshooting": "Merci de soumettre l'erreur générée par le serveur. Pour une installation snap, lancer `sudo snap logs wekan.wekan`. Pour une installation docker, lancer `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Titre (Alphabétiquement)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Pièces jointes",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Si obteniu errors en importar el tauler, de vegades la importació encara funciona, i el tauler es troba a la pàgina Tots els taulers.",
|
||||
"import-json-placeholder": "Aferra codi JSON vàlid aquí",
|
||||
"import-csv-placeholder": "Enganxeu aquí les vostres dades CSV/TSV vàlides",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mapeja els membres",
|
||||
"import-members-map": "El vostre tauler importat té alguns membres. Assigna els membres que vols importar als teus usuaris",
|
||||
"import-members-map-note": "Nota: els membres no assignats s'assignaran a l'usuari actual.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Ordenar fitxes",
|
||||
"due-date": "Data de venciment",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Error del servidor",
|
||||
"server-error-troubleshooting": "Envieu l'error generat pel servidor.\nPer a una instal·lació instantània, executeu: `sudo snap logs wekan.wekan`\nPer a una instal·lació de Docker, executeu: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Títol (alfabèticament)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Adjunts",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Tipus",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Si obteniu errors en importar el tauler, de vegades la importació encara funciona, i el tauler es troba a la pàgina Tots els taulers.",
|
||||
"import-json-placeholder": "Aferra codi JSON vàlid aquí",
|
||||
"import-csv-placeholder": "Enganxeu aquí les vostres dades CSV/TSV vàlides",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mapeja els membres",
|
||||
"import-members-map": "El vostre tauler importat té alguns membres. Assigna els membres que vols importar als teus usuaris",
|
||||
"import-members-map-note": "Nota: els membres no assignats s'assignaran a l'usuari actual.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Ordenamiento activado",
|
||||
"cardsSortPopup-title": "Ordenar fitxes",
|
||||
"due-date": "Data de venciment",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Error del servidor",
|
||||
"server-error-troubleshooting": "Envieu l'error generat pel servidor.\nPer a una instal·lació instantània, executeu: `sudo snap logs wekan.wekan`\nPer a una instal·lació de Docker, executeu: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Títol (alfabèticament)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Adjunts",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Tipus",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Fallo al detener las migraciones",
|
||||
"migration-stopped": "Las migraciones se han detenido correctamente",
|
||||
"mongodb-gridfs-storage": "Almacenamiento MongoDB GridFS",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pausar todas las migraciones",
|
||||
"s3-access-key": "Clave de acceso de S3",
|
||||
"s3-access-key-description": "Clave de acceso de AWS S3 para autentificación",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Si obteniu errors en importar el tauler, de vegades la importació encara funciona, i el tauler es troba a la pàgina Tots els taulers.",
|
||||
"import-json-placeholder": "Aferra codi JSON vàlid aquí",
|
||||
"import-csv-placeholder": "Enganxeu aquí les vostres dades CSV/TSV vàlides",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mapeja els membres",
|
||||
"import-members-map": "El vostre tauler importat té alguns membres. Assigna els membres que vols importar als teus usuaris",
|
||||
"import-members-map-note": "Nota: els membres no assignats s'assignaran a l'usuari actual.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Ordenar fitxes",
|
||||
"due-date": "Data de venciment",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Error del servidor",
|
||||
"server-error-troubleshooting": "Envieu l'error generat pel servidor.\nPer a una instal·lació instantània, executeu: `sudo snap logs wekan.wekan`\nPer a una instal·lació de Docker, executeu: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Títol (alfabèticament)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Adjunts",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Tipus",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "如果导入板卡时出现错误,有时导入仍然有效,并且板卡位于“所有板卡”页面。",
|
||||
"import-json-placeholder": "将有效的 JSON 数据粘贴到此处",
|
||||
"import-csv-placeholder": "在此处粘贴您的有效 CSV/TSV 数据",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "地图成员",
|
||||
"import-members-map": "您导入的董事会有一些成员。请将您要导入的成员映射到您的用户",
|
||||
"import-members-map-note": "注意:未映射的成员将分配给当前用户。",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "排序已开启",
|
||||
"cardsSortPopup-title": "卡片排序",
|
||||
"due-date": "到期日",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "服务器错误",
|
||||
"server-error-troubleshooting": "请提交服务器产生的错误。\n对于快照安装,请运行:`sudo snap logs wekan.wekan`\n对于 Docker 安装,请运行:`sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "标题(按字母顺序)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "附件",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "类型",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "无法停止迁移",
|
||||
"migration-stopped": "迁移已成功停止",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS 存储",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "暂停所有迁移",
|
||||
"s3-access-key": "S3 访问密钥",
|
||||
"s3-access-key-description": "用于身份验证的 AWS S3 访问密钥",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Někdy import funguje i přestože dostáváte chyby při importování tabla, které se zobrazí na stránce Všechna tabla.",
|
||||
"import-json-placeholder": "Sem vlož validní JSON data",
|
||||
"import-csv-placeholder": "Zde vložte vaše platné CSV/TSV data",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mapovat členy",
|
||||
"import-members-map": "Toto importované tablo obsahuje několik osob. Prosím namapujte osoby z importu na místní uživatelské účty.",
|
||||
"import-members-map-note": "Modifikováno",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Řazení je zapnuté",
|
||||
"cardsSortPopup-title": "Třídit",
|
||||
"due-date": "Požadovaný termín",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Chyba na serveru",
|
||||
"server-error-troubleshooting": "Odešlete prosím chybu, kterou server vygeneroval.\nV případě instalace přes snap spusťte: `sudo snap logs wekan.wekan`\nV případě instalace přes Docker spusťte: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Nadpis (Abecedně)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Přílohy",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Typ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Někdy import funguje i přestože dostáváte chyby při importování tabla, které se zobrazí na stránce Všechna tabla.",
|
||||
"import-json-placeholder": "Sem vlož validní JSON data",
|
||||
"import-csv-placeholder": "Zde vložte vaše platné CSV/TSV data",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mapovat členy",
|
||||
"import-members-map": "Toto importované tablo obsahuje několik osob. Prosím namapujte osoby z importu na místní uživatelské účty.",
|
||||
"import-members-map-note": "Modifikováno",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Řazení je zapnuté",
|
||||
"cardsSortPopup-title": "Třídit",
|
||||
"due-date": "Požadovaný termín",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Chyba na serveru",
|
||||
"server-error-troubleshooting": "Odešlete prosím chybu, kterou server vygeneroval.\nV případě instalace přes snap spusťte: `sudo snap logs wekan.wekan`\nV případě instalace přes Docker spusťte: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Nadpis (Abecedně)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Přílohy",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Typ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Os cewch wallau wrth fewnforio bwrdd, weithiau mae mewnforio yn dal i weithio, ac mae bwrdd ar dudalen All Boards.",
|
||||
"import-json-placeholder": "Gludwch eich data JSON dilys yma",
|
||||
"import-csv-placeholder": "Gludwch eich data CSV/TSV dilys yma",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Aelodau map",
|
||||
"import-members-map": "Mae gan eich bwrdd wedi'i fewnforio rai aelodau. Mapiwch yr aelodau yr ydych am eu mewnforio i'ch defnyddwyr",
|
||||
"import-members-map-note": "Nodyn: Bydd aelodau heb eu mapio yn cael eu neilltuo i'r defnyddiwr presennol.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Mae trefn ymlaen",
|
||||
"cardsSortPopup-title": "Cardiau Didoli",
|
||||
"due-date": "Dyddiad Cwblhau",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Gwall Gweinydd",
|
||||
"server-error-troubleshooting": "Cyflwynwch y gwall a gynhyrchwyd gan y gweinydd.\nAr gyfer gosodiad snap, rhedwch: `sudo snap logs wekan.wekan`\nAr gyfer gosodiad Docker, rhedwch: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Teitl (Yn nhrefn yr wyddor)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Ymlyniadau",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Math",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Wedi methu ag atal mudo",
|
||||
"migration-stopped": "Daeth y mudo i ben yn llwyddiannus",
|
||||
"mongodb-gridfs-storage": "Storio GridFS MongoDB",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Oedwch Pob Ymfudiad",
|
||||
"s3-access-key": "Allwedd Mynediad S3",
|
||||
"s3-access-key-description": "Allwedd mynediad AWS S3 ar gyfer dilysu",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Os cewch wallau wrth fewnforio bwrdd, weithiau mae mewnforio yn dal i weithio, ac mae bwrdd ar dudalen All Boards.",
|
||||
"import-json-placeholder": "Gludwch eich data JSON dilys yma",
|
||||
"import-csv-placeholder": "Gludwch eich data CSV/TSV dilys yma",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Aelodau map",
|
||||
"import-members-map": "Mae gan eich bwrdd wedi'i fewnforio rai aelodau. Mapiwch yr aelodau yr ydych am eu mewnforio i'ch defnyddwyr",
|
||||
"import-members-map-note": "Nodyn: Bydd aelodau heb eu mapio yn cael eu neilltuo i'r defnyddiwr presennol.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Mae trefn ymlaen",
|
||||
"cardsSortPopup-title": "Cardiau Didoli",
|
||||
"due-date": "Dyddiad Cwblhau",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Gwall Gweinydd",
|
||||
"server-error-troubleshooting": "Cyflwynwch y gwall a gynhyrchwyd gan y gweinydd.\nAr gyfer gosodiad snap, rhedwch: `sudo snap logs wekan.wekan`\nAr gyfer gosodiad Docker, rhedwch: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Teitl (Yn nhrefn yr wyddor)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Ymlyniadau",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Math",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Wedi methu ag atal mudo",
|
||||
"migration-stopped": "Daeth y mudo i ben yn llwyddiannus",
|
||||
"mongodb-gridfs-storage": "Storio GridFS MongoDB",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Oedwch Pob Ymfudiad",
|
||||
"s3-access-key": "Allwedd Mynediad S3",
|
||||
"s3-access-key-description": "Allwedd mynediad AWS S3 ar gyfer dilysu",
|
||||
|
||||
+345
-243
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"accept": "Acceptér",
|
||||
"act-activity-notify": "Aktivitetsnotits",
|
||||
"accept": "Accepter",
|
||||
"act-activity-notify": "Aktivitetsnotifikation",
|
||||
"act-addAttachment": "tilføjede vedhæftningen __attachment__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-deleteAttachment": "slettede vedhæftning __attachment__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-addSubtask": "tilføjede delopgaven __subtask__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
@@ -9,12 +9,12 @@
|
||||
"act-removeLabel": "Fjernede etiketten __label__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på kortet __board__",
|
||||
"act-removedLabel": "Fjernede etiketten __label__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på kortet __board__",
|
||||
"act-addChecklist": "tilføjede tjeklisten __checklist__ til kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-addChecklistItem": "tilføjede elementet i tjekliste __checklistItem__ til tjeklisten __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-addChecklistItem": "tilføjede elementet i tjeklisten __checklistItem__ til tjeklisten __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-removeChecklist": "fjernede tjeklisten __checklist__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-removeChecklistItem": "fjernede elementet i tjekliste __checklistItem__ fra tjeklisten __checkList__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-removeChecklistItem": "fjernede elementet i tjeklisten __checklistItem__ fra tjeklisten __checkList__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-checkedItem": "markerede __checklistItem__ fra tjeklisten __checklist__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-uncheckedItem": "afmarkerede __checklistItem__ fra tjeklisten __checklist__ fra kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-completeChecklist": "afsluttet tjekliste __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-completeChecklist": "tjekliste fuldført __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-uncompleteChecklist": "uafsluttet tjekliste __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-addComment": "kommenterede på kortet __card__: __comment__ til listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-editComment": "redigerede kommentar på kortet __card__: __comment__ til listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
@@ -26,11 +26,11 @@
|
||||
"act-deleteCustomField": "slettede brugerdefineret felt __customField__ på tavlen __board__",
|
||||
"act-setCustomField": "redigerede brugerdefineret felt __customField__: __customFieldValue__ i kortet __card__ på lsten __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-createList": "tilføjede listen __list__ til tavlen __board__",
|
||||
"act-addBoardMember": "tilføejede medlemmet __member__ til tavlen __board__",
|
||||
"act-addBoardMember": "tilføjede medlemmet __member__ til tavlen __board__",
|
||||
"act-archivedBoard": "Tavlen __board__ blev flyttet til Arkiv",
|
||||
"act-archivedCard": "Kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til arkivet",
|
||||
"act-archivedList": "Listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til arkivet",
|
||||
"act-archivedSwimlane": "Svømmebanen __swimlane__ på tavlen __board__ er flyttet til arkivet",
|
||||
"act-archivedCard": "Kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til Arkiv",
|
||||
"act-archivedList": "Listen __list__ i svømmebanen __swimlane__ på tavlen __board__ er flyttet til Arkiv",
|
||||
"act-archivedSwimlane": "Svømmebanen __swimlane__ på tavlen __board__ er flyttet til Arkiv",
|
||||
"act-importBoard": "importerede tavlen __board__",
|
||||
"act-importCard": "importerede kortet __card__ til listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"act-importList": "importerede listen __list__ til svømmebanen __swimlane__ på tavlen __board__",
|
||||
@@ -49,80 +49,80 @@
|
||||
"activity-archived": "%s flyttet til Arkiv",
|
||||
"activity-attached": "vedhæftede %s til %s",
|
||||
"activity-created": "oprettede %s",
|
||||
"activity-changedListTitle": "döpte om listan till %s",
|
||||
"activity-changedListTitle": "omdøbte listen til %s",
|
||||
"activity-customfield-created": "oprettede brugerdefineret felt %s",
|
||||
"activity-excluded": "ekskluderet %s fra %s",
|
||||
"activity-imported": "importerede %s ind i %s fra %s",
|
||||
"activity-excluded": "ekskluderede %s fra %s",
|
||||
"activity-imported": "importerede %s til %s fra %s",
|
||||
"activity-imported-board": "importerede %s fra %s",
|
||||
"activity-joined": "indgik i %s",
|
||||
"activity-joined": "sammenlagt med %s",
|
||||
"activity-moved": "flyttede %s fra %s til %s",
|
||||
"activity-on": "per %s",
|
||||
"activity-removed": "fjernede %s fra %s",
|
||||
"activity-sent": "sendte %s til %s",
|
||||
"activity-unjoined": "udgik fra %s",
|
||||
"activity-unjoined": "adskilt fra %s",
|
||||
"activity-subtask-added": "tilføjede delopgave til %s",
|
||||
"activity-checked-item": "afkrydsede %s i tjeklisten %s af %s",
|
||||
"activity-checked-item": "markerede %s i tjeklisten %s af %s",
|
||||
"activity-unchecked-item": "fjernede kryds %s i tjeklisten %s af %s",
|
||||
"activity-checklist-added": "tilføjede tjeklisten til %s",
|
||||
"activity-checklist-removed": "fjernede en tjekliste fra %s",
|
||||
"activity-checklist-completed": "færdiggjorde tjekliste %s af %s",
|
||||
"activity-checklist-uncompleted": "gjorde tjeklisten ukomplet, %s af %s",
|
||||
"activity-checklist-uncompleted": "ændrede tjekliste %s af %s til ikke-færdiggjort",
|
||||
"activity-checklist-item-added": "tilføjede element i tjekliste til '%s' i %s",
|
||||
"activity-checklist-item-removed": "fjernede element i tjekliste fra '%s' i %s",
|
||||
"add": "Tilføj",
|
||||
"activity-checked-item-card": "markerede %s i tjeklisten %s",
|
||||
"activity-unchecked-item-card": "afmarkerede %s i tjeklisten %s",
|
||||
"activity-checklist-completed-card": "afsluttet tjekliste __checklist__ i kortet __card__ på listen __list__ i svømmebanen __swimlane__ på tavlen __board__",
|
||||
"activity-checklist-uncompleted-card": "gjorde tjeklisten ukomplet %s",
|
||||
"activity-checklist-uncompleted-card": "ændrede tjekliste %s til ikke-færdiggjort",
|
||||
"activity-editComment": "redigerede kommentar %s",
|
||||
"activity-deleteComment": "slettede kommentar %s",
|
||||
"activity-receivedDate": "redigerade mottaget datum till %s av %s",
|
||||
"activity-startDate": "redigerade startdatum till %s av %s",
|
||||
"allboards.starred": " Stjärnmärkt",
|
||||
"activity-receivedDate": "redigerede modtaget datoen til %s af %s",
|
||||
"activity-startDate": "redigerede startdatoen til %s af %s",
|
||||
"allboards.starred": "Stjernemarkeret",
|
||||
"allboards.templates": "Skabeloner",
|
||||
"allboards.remaining": "Återstående",
|
||||
"allboards.workspaces": "Arbetsytor",
|
||||
"allboards.add-workspace": "Lägg till arbetsyta",
|
||||
"allboards.add-workspace-prompt": "Arbetsytans namn",
|
||||
"allboards.add-subworkspace": "Lägg till underarbetsyta",
|
||||
"allboards.add-subworkspace-prompt": "Underarbetsytans namn",
|
||||
"allboards.edit-workspace": "Redigera arbetsyta",
|
||||
"allboards.edit-workspace-name": "Arbetsytans namn",
|
||||
"allboards.edit-workspace-icon": "Arbetsytans ikon (markdown)",
|
||||
"allboards.workspace-menu": "Arbetsytans meny",
|
||||
"workspace-settings": "Arbetsytans inställningar",
|
||||
"workspaceActionsPopup-title": "Arbetsytans inställningar",
|
||||
"allboards.workspace-color": "Färg",
|
||||
"allboards.delete-workspace-confirm": "Är du säker på att du vill ta bort den här arbetsytan?",
|
||||
"allboards.delete-workspace-confirm-check": "Bekräfta borttagning av arbetsyta",
|
||||
"multi-selection-active": "Klicka i kryssrutor för att välja tavlor",
|
||||
"activity-dueDate": "redigerade förfallodag till %s av %s",
|
||||
"activity-endDate": "redigerade slutdatum till %s av %s",
|
||||
"allboards.remaining": "Udestående",
|
||||
"allboards.workspaces": "Arbejdsområder",
|
||||
"allboards.add-workspace": "Tilføj Arbejdsområde",
|
||||
"allboards.add-workspace-prompt": "Arbejdsområdets navn",
|
||||
"allboards.add-subworkspace": "Tilføj Underarbejdsområde",
|
||||
"allboards.add-subworkspace-prompt": "Underarbejdsområdets navn",
|
||||
"allboards.edit-workspace": "Rediger arbejdsområde",
|
||||
"allboards.edit-workspace-name": "Arbejdsområdets navn",
|
||||
"allboards.edit-workspace-icon": "Arbejdsområdets ikon (markdown)",
|
||||
"allboards.workspace-menu": "Arbejdsområdets menu",
|
||||
"workspace-settings": "Arbejdsområdets indstillinger",
|
||||
"workspaceActionsPopup-title": "Arbejdsområdets indstillinger",
|
||||
"allboards.workspace-color": "Farve",
|
||||
"allboards.delete-workspace-confirm": "Er du sikker på, at du vil slette dette arbejdsområde?",
|
||||
"allboards.delete-workspace-confirm-check": "Bekræft sletning af arbejdsområdet",
|
||||
"multi-selection-active": "Klik på tjekboksene for at vælge tavler",
|
||||
"activity-dueDate": "Redigerede forfaldsdato til %s af %s",
|
||||
"activity-endDate": "Redigerede slutdato til %s af %s",
|
||||
"add-attachment": "Tilføj vedhæftning",
|
||||
"add-board": "Tilføj tavle",
|
||||
"add-template": "Lägg till mall",
|
||||
"add-card": "Tilføj kort",
|
||||
"add-card-to-top-of-list": "Lägg till kort överst i listan",
|
||||
"add-card-to-bottom-of-list": "Lägg till kort i botten av listan",
|
||||
"add-card-to-top-of-list": "Tilføj kort øverst i listen",
|
||||
"add-card-to-bottom-of-list": "Tilføj kort nederst i listen",
|
||||
"addListPopup-title": "Tilføj liste",
|
||||
"setListWidthPopup-title": "Ställ in min bredd",
|
||||
"set-list-width": "Ställ in min bredd",
|
||||
"set-list-width-value": "Min och max bredd (pixlar)",
|
||||
"list-width-error-message": "Listbredder måste vara heltal större än 100",
|
||||
"keyboard-shortcuts-enabled": "Tangentbordsgenvägar aktiverade. Klicka för att inaktivera.",
|
||||
"keyboard-shortcuts-disabled": "Tangentbordsgenvägar inaktiverade. Klicka för att aktivera.",
|
||||
"setListWidthPopup-title": "Indstil bredder",
|
||||
"set-list-width": "Indstil bredder",
|
||||
"set-list-width-value": "Indstil min. og maks. bredder (pixels)",
|
||||
"list-width-error-message": "Listebredder skal være heltal større end 100",
|
||||
"keyboard-shortcuts-enabled": "Tastaturgenveje aktiverede. Klik for at deaktivere.",
|
||||
"keyboard-shortcuts-disabled": "Tastaturgenveje deaktiverede. Klik for at aktivere.",
|
||||
"setSwimlaneHeightPopup-title": "Angiv højde på svømmebane",
|
||||
"set-swimlane-height": "Angiv højde på svømmebane",
|
||||
"set-swimlane-height-value": "Svømmebanes bredde (pixels)",
|
||||
"swimlane-height-error-message": "Svømmebreddes højde skal være et positivt heltal",
|
||||
"swimlane-height-error-message": "Svømmebanens højde skal være et positivt heltal",
|
||||
"add-swimlane": "Tilføj svømmebane",
|
||||
"add-subtask": "Tilføj delopgave",
|
||||
"add-checklist": "Tilføj tjekliste",
|
||||
"checklist": "Checklista",
|
||||
"checklist": "Tjekliste",
|
||||
"add-checklist-item": "Tilføj et element til tjeklisten",
|
||||
"close-add-checklist-item": "Stäng lägg till en rad i checklistans formulär",
|
||||
"close-edit-checklist-item": "Stäng ändra en rad i checklistans formulär",
|
||||
"convertChecklistItemToCardPopup-title": "Ändra till kort",
|
||||
"close-add-checklist-item": "Luk tilføj et element til tjeklisteformular",
|
||||
"close-edit-checklist-item": "Luk ændre et element til tjeklisteformular",
|
||||
"convertChecklistItemToCardPopup-title": "Ændre til kort",
|
||||
"add-cover": "Tilføj coverbillede til minikort",
|
||||
"add-label": "Tilføj etikette",
|
||||
"add-list": "Tilføj liste",
|
||||
@@ -131,8 +131,8 @@
|
||||
"added": "Tilføjet",
|
||||
"addMemberPopup-title": "Tilføj medlemmer",
|
||||
"memberPopup-title": "Medlemsindstillinger",
|
||||
"admin": "Adminstratör",
|
||||
"admin-desc": "Kan visa och redigera kort, ta bort medlemmar och ändra inställningar för tavlan. Kan visa aktiviteter.",
|
||||
"admin": "Administrator",
|
||||
"admin-desc": "Kan vise of redigere kort, fjerne medlemmer og ændre indstillinger for tavlen. Kan vise aktiviteter.",
|
||||
"admin-announcement": "Annoncering",
|
||||
"admin-announcement-active": "Aktivér annoncering på tværs af systemet",
|
||||
"admin-announcement-title": "Annoncering fra administrator",
|
||||
@@ -140,12 +140,12 @@
|
||||
"and-n-other-card": "Samt __count__ andre kort",
|
||||
"and-n-other-card_plural": "Samt __count__ andre kort",
|
||||
"apply": "Anvend",
|
||||
"app-is-offline": "Indlæser, vent venligst. Genopfriskes siden er der risiko for tab af data. Fungerer indlæsningen ikke, så tjek venligst om serveren er stoppet.",
|
||||
"app-try-reconnect": "Försök att återansluta",
|
||||
"app-is-offline": "Indlæser, vent venligst. Hvis siden genopfriskes, er der risiko for tab af data. Fungerer indlæsningen ikke, så tjek venligst om serveren er stoppet.",
|
||||
"app-try-reconnect": "Prøv at genoprette forbindelsen.",
|
||||
"archive": "Flyt til arkiv",
|
||||
"archive-all": "Flyt alle til arkiv",
|
||||
"archive-board": "Flyt tavle til arkiv",
|
||||
"archive-board-confirm": " Är du säker på att du vill arkivera den här tavlan?",
|
||||
"archive-board-confirm": "Er du sikker på, at du vil arkivere denne tavle?",
|
||||
"archive-card": "Flyt kort til arkiv",
|
||||
"archive-list": "Flyt liste til arkiv",
|
||||
"archive-swimlane": "Flyt svømmebane til arkiv",
|
||||
@@ -158,12 +158,12 @@
|
||||
"archives": "Arkiv",
|
||||
"template": "Skabelon",
|
||||
"templates": "Skabeloner",
|
||||
"template-container": "Mallbibliotek",
|
||||
"add-template-container": "Lägg till från mall",
|
||||
"assign-member": "Tilknyt medlem",
|
||||
"template-container": "Skabelonsamling",
|
||||
"add-template-container": "Tilføj skabelonsamling",
|
||||
"assign-member": "Tildel medlem",
|
||||
"attached": "vedhæftet",
|
||||
"attachment": "Vedhæftning",
|
||||
"attachment-delete-pop": "Slettes en vedhæftning sker det permanent. Det kan ikke omgøres.",
|
||||
"attachment-delete-pop": "Slettes en vedhæftning, sker det permanent. Det kan ikke fortrydes efterfølgende.",
|
||||
"attachmentDeletePopup-title": "Slet vedhæftning?",
|
||||
"attachments": "Vedhæftninger",
|
||||
"auto-watch": "Følg automatisk tavler når de oprettes",
|
||||
@@ -171,25 +171,25 @@
|
||||
"back": "Tilbage",
|
||||
"board-change-color": "Skift farve",
|
||||
"board-change-background-image": "Skift baggrundsbillede",
|
||||
"board-background-image-url": "Bakgrundsbildens URL",
|
||||
"add-background-image": "Lägg till bakgrundsbild",
|
||||
"remove-background-image": "Ta bort bakgrundsbild",
|
||||
"show-at-all-boards-page": "Visa på sidan alla tavlor",
|
||||
"board-info-on-my-boards": "Alla tavlors inställningar",
|
||||
"boardInfoOnMyBoardsPopup-title": "Alla tavlors inställningar",
|
||||
"boardInfoOnMyBoards-title": "Alla tavlors inställningar",
|
||||
"show-card-counter-per-list": "Visa antal kort per lista",
|
||||
"show-board_members-avatar": "Visa medlemmarnas avatarer",
|
||||
"board_members": "Alla tavlans medlemmar",
|
||||
"card_members": "Alla medlemmar i detta kort på tavlan",
|
||||
"board_assignees": " Alla tilldelade i alla kort på tavlan",
|
||||
"card_assignees": "Alla tilldelade i detta kort på tavlan",
|
||||
"board-background-image-url": "Baggrundsbilledets URL",
|
||||
"add-background-image": "Tilføj baggrundsbillede",
|
||||
"remove-background-image": "Fjern baggrundsbillede",
|
||||
"show-at-all-boards-page": "Vis på siden Alle Tavler",
|
||||
"board-info-on-my-boards": "Indstillinger for Alle Tavler",
|
||||
"boardInfoOnMyBoardsPopup-title": "Indstillinger for Alle Tavler",
|
||||
"boardInfoOnMyBoards-title": "Indstillinger for Alle Tavler",
|
||||
"show-card-counter-per-list": "Vis antal kort per liste",
|
||||
"show-board_members-avatar": "Vis avatarer for medlemmer",
|
||||
"board_members": "Alle tavlens medlemmer",
|
||||
"card_members": "Alle medlemmer for dette kort på tavlen",
|
||||
"board_assignees": "Alle tildelte medlemmer på alle kort på tavlen",
|
||||
"card_assignees": "Alle tildelte medlemmer på dette kort på tavlen",
|
||||
"board-nb-stars": "%s stjerner",
|
||||
"board-not-found": "Fandt ikke tavle",
|
||||
"board-not-found": "Tavlen blev ikke fundet",
|
||||
"board-private-info": "Denne tavle vil være <strong>privat</strong>.",
|
||||
"board-public-info": "Denne tavle vil være <strong>offentlig</strong>.",
|
||||
"board-drag-drop-reorder-or-click-open": "Dra och släpp för att ändra ordning på tavelsymboler. Klicka på tavelsymbolen för att öppna tavlan.",
|
||||
"board-open-and-move-between-remaining-and-workspaces": "Click to open board. Use drag handle to assign board to __workspaces__ (drop on workspace in sidebar).",
|
||||
"board-drag-drop-reorder-or-click-open": "Træk og slip for at arrangere tavleikoner. Klik på tavleikon for at åbne tavle.",
|
||||
"board-open-and-move-between-remaining-and-workspaces": "Klik for at åbne tavle. Træk-og-slip for at tildele tavle til __workspaces__ (slip på Arbejdsområde i sidemenuen).",
|
||||
"boardChangeColorPopup-title": "Skift tavlens baggrund",
|
||||
"boardChangeBackgroundImagePopup-title": "Skift baggrundsbillede",
|
||||
"allBoardsChangeColorPopup-title": "Skift farve",
|
||||
@@ -202,37 +202,37 @@
|
||||
"boardChangeViewPopup-title": "Tavlevisning",
|
||||
"boards": "Tavler",
|
||||
"board-view": "Tavlevisning",
|
||||
"desktop-mode": " Skrivbordsläge",
|
||||
"mobile-mode": "Mobilläge",
|
||||
"mobile-desktop-toggle": " Växla mellan mobilläge och skrivbordsläge",
|
||||
"zoom-in": "Zooma in",
|
||||
"zoom-out": "Zooma ut",
|
||||
"click-to-change-zoom": " Klicka för att ändra zoomnivå",
|
||||
"zoom-level": "Zoomnivå",
|
||||
"enter-zoom-level": "Ange zoomnivå (50-300%):",
|
||||
"desktop-mode": "Skrivebordstilstand",
|
||||
"mobile-mode": "Mobiltilstand",
|
||||
"mobile-desktop-toggle": "Skift imellem mobil- og skrivebordstilstand",
|
||||
"zoom-in": "Zoom ind",
|
||||
"zoom-out": "Zoom ud",
|
||||
"click-to-change-zoom": "Klik for at ændre zoom niveau",
|
||||
"zoom-level": "Zoom niveau",
|
||||
"enter-zoom-level": "Angiv zoom niveau (50-300%):",
|
||||
"board-view-cal": "Kalender",
|
||||
"board-view-swimlanes": "Svømmebaner",
|
||||
"board-view-collapse": "Sammenfold",
|
||||
"board-view-gantt": "Gantt",
|
||||
"board-view-lists": "Lister",
|
||||
"bucket-example": " Som till exempel \"Bucket List\"",
|
||||
"calendar-previous-month-label": " Föregående månad",
|
||||
"calendar-next-month-label": " Nästa månad",
|
||||
"cancel": "Annullér",
|
||||
"card-archived": "Dette kort blev flyttet til arkivet.",
|
||||
"board-archived": "Denne tavle blev flyttet til arkivet.",
|
||||
"bucket-example": "F.eks. \"Bucket List\"",
|
||||
"calendar-previous-month-label": "Forgående måned",
|
||||
"calendar-next-month-label": "Næste måned",
|
||||
"cancel": "Annuller",
|
||||
"card-archived": "Dette kort blev flyttet til Arkiv.",
|
||||
"board-archived": "Denne tavle blev flyttet til Arkiv.",
|
||||
"card-comments-title": "Dette kort har %s kommentar.",
|
||||
"card-delete-notice": "Sletning vil være permanent. Du mister alle handlinger knyttet til dette kort.",
|
||||
"card-delete-pop": "Alle handlinger vil blive fjernet fra aktivitetsfeedet, og du kan ikke genåbne kortet. Det kan ikke omgøres.",
|
||||
"card-delete-suggest-archive": "Du kan flytte et kort til arkivet for at fjerne det fra tavlen, og bevare aktiviteten.",
|
||||
"card-archive-pop": "Kortet kommer inte synas i denna listan efter det är arkiverat.",
|
||||
"card-archive-suggest-cancel": "Du kan återställa kort från Arkivet",
|
||||
"list-archive-pop": "Listan kommer inte att synas på tavlan efter arkivering.",
|
||||
"list-archive-suggest": "Du kan senare återställa listan från Arkivet i Tavelinställningar.",
|
||||
"listArchivePopup-title": "Arkivera lista?",
|
||||
"swimlane-archive-pop": "Simbana kommer inte att synas på tavlan efter arkivering.",
|
||||
"swimlane-archive-suggest": "Du kan senare återställa simbanan från Arkivet i Tavelinställningar.",
|
||||
"swimlaneArchivePopup-title": "Arkivera simbana?",
|
||||
"card-delete-pop": "Alle handlinger vil blive fjernet fra aktivitetsfeedet, og du kan ikke genåbne kortet. Det kan ikke fortrydes efterfølgende.",
|
||||
"card-delete-suggest-archive": "Du kan flytte et kort til arkivet for at fjerne det fra tavlen og bevare aktiviteten.",
|
||||
"card-archive-pop": "Kortet vil ikke være synligt på denne liste efter arkivering.",
|
||||
"card-archive-suggest-cancel": "Du kan gendanne kortet senere fra Arkiv.",
|
||||
"list-archive-pop": "Listen vil ikke være synligt på denne liste efter arkivering.",
|
||||
"list-archive-suggest": "Du kan gendanne listen senere fra Tavleindstillinger.",
|
||||
"listArchivePopup-title": "Arkiver liste?",
|
||||
"swimlane-archive-pop": "Svømmebane vil ikke være synligt på denne liste efter arkivering.",
|
||||
"swimlane-archive-suggest": "Du kan gendanne svømmebanen senere fra Tavleindstillinger.",
|
||||
"swimlaneArchivePopup-title": "Arkiver svømmebane?",
|
||||
"card-due": "Forfalder",
|
||||
"card-due-on": "Forfaldsdato",
|
||||
"card-spent": "Anvendt tid",
|
||||
@@ -240,10 +240,10 @@
|
||||
"card-edit-custom-fields": "Redigér brugerdefinerede felter",
|
||||
"card-edit-labels": "Redigér etiketter",
|
||||
"card-edit-members": "Redigér medlemmer",
|
||||
"card-labels-title": "Ændr etiketter for kortet.",
|
||||
"card-labels-title": "Ændre etiketter for kortet.",
|
||||
"card-members-title": "Tilføj eller fjern medlemmer på tavlen fra kortet.",
|
||||
"card-start": "Påbörjades",
|
||||
"card-start-on": "Starter per",
|
||||
"card-start": "Påbegynd",
|
||||
"card-start-on": "Påbegyndes per",
|
||||
"cardAttachmentsPopup-title": "Vedhæft fra",
|
||||
"cardCustomField-datePopup-title": "Ændringsdato",
|
||||
"cardCustomFieldsPopup-title": "Redigér brugerdefinerede felter",
|
||||
@@ -259,9 +259,9 @@
|
||||
"vote-against": "går imod",
|
||||
"deleteVotePopup-title": "Slet afstemning?",
|
||||
"vote-delete-pop": "Sletning er permanent. Du mister alle handlinger knyttet til denne afstemning.",
|
||||
"cardStartPlanningPokerPopup-title": "Starta en planeringspoker.",
|
||||
"card-edit-planning-poker": "Ändra planeringspoker",
|
||||
"editPokerEndDatePopup-title": "Ändra planeringspokerns sista röstdag",
|
||||
"cardStartPlanningPokerPopup-title": "Opret en rangering",
|
||||
"card-edit-planning-poker": "Ændre rangering",
|
||||
"editPokerEndDatePopup-title": "Ændre rangeringsafstemningens slutdato",
|
||||
"poker-question": "Rangering",
|
||||
"poker-one": "1",
|
||||
"poker-two": "2",
|
||||
@@ -273,15 +273,15 @@
|
||||
"poker-forty": "40",
|
||||
"poker-oneHundred": "100",
|
||||
"poker-unsure": "?",
|
||||
"poker-finish": "Klar",
|
||||
"poker-result-votes": "Röster",
|
||||
"poker-result-who": "Vem",
|
||||
"poker-replay": "Upprepa",
|
||||
"set-estimation": "Ange uppskattning",
|
||||
"deletePokerPopup-title": "Radera planeringspoker?",
|
||||
"poker-delete-pop": "Raderingen är permanent. Du vill förlora alla associationer med planneringspokern.",
|
||||
"poker-finish": "Afslut",
|
||||
"poker-result-votes": "Stemmer",
|
||||
"poker-result-who": "Hvem",
|
||||
"poker-replay": "Gentag",
|
||||
"set-estimation": "Angiv estimering",
|
||||
"deletePokerPopup-title": "Vil du slette rangeringen?",
|
||||
"poker-delete-pop": "Sletningen er permanent. Du vil miste alle handlinger associeret med denne rangering.",
|
||||
"cardDeletePopup-title": "Slet kort?",
|
||||
"cardArchivePopup-title": "Arkivera kort?",
|
||||
"cardArchivePopup-title": "Arkiver kort?",
|
||||
"cardDetailsActionsPopup-title": "Handlinger for kort",
|
||||
"cardLabelsPopup-title": "Etiketter",
|
||||
"cardMembersPopup-title": "Medlemmer",
|
||||
@@ -300,8 +300,8 @@
|
||||
"change-permissions": "Tilpas tilladelser",
|
||||
"change-settings": "Tilpas indstillinger",
|
||||
"changeAvatarPopup-title": "Tilpas avatar",
|
||||
"delete-avatar-confirm": "Är du säker på att du vill ta bort denna avatar?",
|
||||
"deleteAvatarPopup-title": "Ta bort avatar?",
|
||||
"delete-avatar-confirm": "Er du sikker på, at du vil slette denne avatar?",
|
||||
"deleteAvatarPopup-title": "Slet avatar?",
|
||||
"changeLanguagePopup-title": "Skift sprog",
|
||||
"changePasswordPopup-title": "Skift kodeord",
|
||||
"changePermissionsPopup-title": "Tilpas tilladelser",
|
||||
@@ -310,17 +310,17 @@
|
||||
"checklists": "Tjeklister",
|
||||
"click-to-star": "Klik for at tilføje stjerne til tavlen.",
|
||||
"click-to-unstar": "Klik for at fjerne stjerne fra tavlen.",
|
||||
"click-to-enable-auto-width": "Automatisk listbredd inaktiverad. Klicka för att aktivera.",
|
||||
"click-to-disable-auto-width": "Automatisk listbredd aktiverad. Klicka för att inaktivera.",
|
||||
"auto-list-width": "Automatisk listbredd",
|
||||
"click-to-enable-auto-width": "Automatisk listebredde deaktiveret. Klik for at aktivere.",
|
||||
"click-to-disable-auto-width": "Automatisk listebredde aktiveret. Klik for at deaktivere.",
|
||||
"auto-list-width": "Automatisk listebredde",
|
||||
"clipboard": "Udklipsholder eller træk-og-slip",
|
||||
"close": "Luk",
|
||||
"close-board": "Luk tavle",
|
||||
"close-board-pop": "Du har mulighed for at genskabe tavlen ved at klikke på \"Arkiv\"-knappen fra overskriften hjem.",
|
||||
"close-card": "Stäng kortet",
|
||||
"close-card": "Luk kortet",
|
||||
"color-black": "sort",
|
||||
"color-blue": "blå",
|
||||
"color-crimson": "crimsonrød",
|
||||
"color-crimson": "karmosinrød",
|
||||
"color-darkgreen": "mørkegrøn",
|
||||
"color-gold": "guld",
|
||||
"color-gray": "grå",
|
||||
@@ -349,38 +349,38 @@
|
||||
"comment-placeholder": "",
|
||||
"comment-only": "Kun kommentarer",
|
||||
"comment-only-desc": "Kan kun kommentere på kort.",
|
||||
"comment-assigned-only": "Endast tilldelad kommentar",
|
||||
"comment-assigned-only-desc": "Endast tilldelade kort synliga. Kan endast kommentera.",
|
||||
"comment-delete": "Är du säker på att du vill radera kommentaren?",
|
||||
"deleteCommentPopup-title": "Radera kommentaren?",
|
||||
"comment-assigned-only": "Kun tildelt kommentarer.",
|
||||
"comment-assigned-only-desc": "Kun tildelte kort er synlige. Kan kun kommentere.",
|
||||
"comment-delete": "Er du sikker på, at du vil slette denne kommentar?",
|
||||
"deleteCommentPopup-title": "Slet kommentaren?",
|
||||
"no-comments": "Ingen kommentarer",
|
||||
"no-comments-desc": "Kan inte se kommentarer.",
|
||||
"read-only": "Skrivskyddad",
|
||||
"read-only-desc": "Kan endast visa kort. Kan inte redigera.",
|
||||
"read-assigned-only": "Endast tilldelad läsning",
|
||||
"read-assigned-only-desc": "Endast tilldelade kort synliga. Kan inte redigera.",
|
||||
"no-comments-desc": "Kan ikke se kommentarer.",
|
||||
"read-only": "Skrivebeskyttet",
|
||||
"read-only-desc": "Kan kun se kort, ikke redigere dem.",
|
||||
"read-assigned-only": "Kun tildelt læsning",
|
||||
"read-assigned-only-desc": "Kun tildelte kort er synlige. Kan ikke redigere.",
|
||||
"worker": "Arbejder",
|
||||
"worker-desc": "Kan kun flytte kort, tildele sig selv til kort og kommentere.",
|
||||
"computer": "Dator",
|
||||
"confirm-subtask-delete-popup": "Er du sikker på du vil slette delopgaven?",
|
||||
"confirm-checklist-delete-popup": "Är du säker på att du vill radera checklistan?",
|
||||
"confirm-checklist-item-delete-popup": "Är du säker på att du vill ta bort checklistepunkten?",
|
||||
"subtaskDeletePopup-title": "Radera deluppgift?",
|
||||
"checklistDeletePopup-title": "Radera checklistan?",
|
||||
"checklistItemDeletePopup-title": "Radera checklistpost?",
|
||||
"confirm-checklist-delete-popup": "Er du sikker på, at du vil slette tjeklisten?",
|
||||
"confirm-checklist-item-delete-popup": "Er du sikker på, at du vil slette dette element på tjeklisten?",
|
||||
"subtaskDeletePopup-title": "Slet delopgave?",
|
||||
"checklistDeletePopup-title": "Slet tjeklisten?",
|
||||
"checklistItemDeletePopup-title": "Slet element på tjeklisten?",
|
||||
"copy-card-link-to-clipboard": "Kopiér link til kort til udklipsholder",
|
||||
"copy-text-to-clipboard": "Kopiera text till urklipp",
|
||||
"copy-text-to-clipboard": "Kopier tekst til udklipsholder",
|
||||
"linkCardPopup-title": "Sammenkæd kort",
|
||||
"searchElementPopup-title": "Søg",
|
||||
"copyCardPopup-title": "Kopiér kort",
|
||||
"copyListPopup-title": "Kopiera lista",
|
||||
"copyManyCardsPopup-title": "Kopiera mall till många kort",
|
||||
"copyCardPopup-title": "Kopier kort",
|
||||
"copyListPopup-title": "Kopier liste",
|
||||
"copyManyCardsPopup-title": "Kopier skabelon til mange kort",
|
||||
"copyManyCardsPopup-instructions": "Destination for kortenes titler og beskrivelser i dette JSON-format",
|
||||
"copyManyCardsPopup-format": "[ {\"title\": \"Första kortets titel\", \"description\":\"Första kortets beskrivning\"}, {\"title\":\"Andra kortets titel\",\"description\":\"Andra kortets beskrivning\"},{\"title\":\"Sista kortets titel\",\"description\":\"Sista kortets beskrivning\"} ]",
|
||||
"copyManyCardsPopup-format": "[ {\"title\": \"Titel på første kort\", \"description\":\"Beskrivelse af første kort\"}, {\"title\":\"Andet korts titel\",\"description\":\"Beskrivelse af andet kort\"},{\"title\":\"Sidste korts titel\",\"description\":\"Beskrivelse af sidste kort\"} ]",
|
||||
"create": "Opret",
|
||||
"createBoardPopup-title": "Opret tavle",
|
||||
"createTemplateContainerPopup-title": "Lägg till från mall",
|
||||
"chooseBoardSourcePopup-title": "Importér tavle",
|
||||
"createTemplateContainerPopup-title": "Tilføj skabelonsamling",
|
||||
"chooseBoardSourcePopup-title": "Importer tavle",
|
||||
"createLabelPopup-title": "Opret etikette",
|
||||
"createCustomField": "Opret felt",
|
||||
"createCustomFieldPopup-title": "Opret felt",
|
||||
@@ -399,7 +399,7 @@
|
||||
"custom-field-text": "Tekst",
|
||||
"custom-fields": "Brugerdefinerede felter",
|
||||
"date": "Dato",
|
||||
"date-format": "Datumformat",
|
||||
"date-format": "Datoformat",
|
||||
"date-format-yyyy-mm-dd": "ÅÅÅÅ-MM-DD",
|
||||
"date-format-dd-mm-yyyy": "DD-MM-ÅÅÅÅ",
|
||||
"date-format-mm-dd-yyyy": "MM-DD-ÅÅÅÅ",
|
||||
@@ -422,13 +422,13 @@
|
||||
"editCardStartDatePopup-title": "Skift startdato",
|
||||
"editCardDueDatePopup-title": "Skift forfaldsdato",
|
||||
"editCustomFieldPopup-title": "Redigér felt",
|
||||
"addReactionPopup-title": "Lägg till reaktion",
|
||||
"addReactionPopup-title": "Tilføj reaktion",
|
||||
"editCardSpentTimePopup-title": "Tilpas forbrugt tid",
|
||||
"editLabelPopup-title": "Skift etikette",
|
||||
"editNotificationPopup-title": "Redigér notifikation",
|
||||
"editProfilePopup-title": "Redigér profil",
|
||||
"email": "E-mail",
|
||||
"email-address": "E-postadress",
|
||||
"email": "Email",
|
||||
"email-address": "Email adresse",
|
||||
"email-enrollAccount-subject": "Der er oprettet konto til dig på __siteName__",
|
||||
"email-enrollAccount-text": "Hej __user__,\n\nFor at begynde at benytte tjenesten, så klik linket nedenfor.\n\n__url__\n\nTak.",
|
||||
"email-fail": "Afsendelse af e-mail mislykkedes",
|
||||
@@ -442,12 +442,12 @@
|
||||
"email-sent": "E-mail er afsendt",
|
||||
"email-verifyEmail-subject": "Verificér din e-mailadresse på your __siteName__",
|
||||
"email-verifyEmail-text": "Hej __user__,\n\nFor at verificere din e-mail for kontoen, så klik på linket nedenfor.\n\n__url__\n\nTak.",
|
||||
"enable-vertical-scrollbars": "Aktivera vertikala rullningslister",
|
||||
"enable-vertical-scrollbars": "Aktiver vertikale rullepaneler",
|
||||
"enable-wip-limit": "Slå WIP-begrænsning til",
|
||||
"error-board-doesNotExist": "Denne tavle eksisterer ikke.",
|
||||
"error-board-notAdmin": "Du skal være administrator for tavlen for at gøre dette",
|
||||
"error-board-notAMember": "Du skal være medlem af denne tavle for at gøre dette",
|
||||
"error-notAllowed": "Your board role is not allowed to do that",
|
||||
"error-notAllowed": "Din tavle-rolle har ikke tilladelse til at udføre denne handling",
|
||||
"error-json-malformed": "Din tekst er ikke gyldig JSON",
|
||||
"error-json-schema": "Dine JSON-data indeholder ikke den rette information i det rette format",
|
||||
"error-csv-schema": "Din CSV(Comma Separated Values)/TSV (Tab Separated Values) innehåller inte korrekt information i rätt format.",
|
||||
@@ -455,77 +455,77 @@
|
||||
"error-user-doesNotExist": "Brugeren findes ikke",
|
||||
"error-user-notAllowSelf": "Du kan ikke invitere dig selv",
|
||||
"error-user-notCreated": "Brugeren er ikke oprettet",
|
||||
"error-username-taken": "Brugernavnet er optaget",
|
||||
"error-orgname-taken": "Organisations namnet är upptaget.",
|
||||
"error-teamname-taken": "Detta team namn är redan taget.",
|
||||
"error-email-taken": "E-mailadressen er allerede optaget",
|
||||
"export-board": "Eksportér tavle",
|
||||
"export-board-json": "Eksportér tavle til JSON",
|
||||
"export-board-csv": "Eksportér tavle til CSV",
|
||||
"export-board-csv-comma": "Exportera till CSV (avgränsare: ,)",
|
||||
"export-board-csv-semicolon": "Exportera till CSV (avgränsare: ;)",
|
||||
"export-board-tsv": "Eksportér tavle til TSV",
|
||||
"export-board-excel": "Exportera tavla till Excel",
|
||||
"user-can-not-export-excel": "Användare kan inte exportera till Excel",
|
||||
"export-board-html": "Eksportér tavle til HTML",
|
||||
"export-card": "Exportera kort",
|
||||
"export-card-pdf": "Exportera kort till PDF",
|
||||
"export-card-excel": "Exportera kort till Excel",
|
||||
"export-card-excel-fields": "Välj fält att inkludera i Excel-export:",
|
||||
"error-username-taken": "Brugernavnet er allerede i brug",
|
||||
"error-orgname-taken": "Organisationsnavnet er allerede i brug",
|
||||
"error-teamname-taken": "Holdnavnet er allerede i brug",
|
||||
"error-email-taken": "Email adressen er allerede i brug",
|
||||
"export-board": "Eksporter tavle",
|
||||
"export-board-json": "Eksporter tavle til JSON",
|
||||
"export-board-csv": "Eksporter tavle til CSV",
|
||||
"export-board-csv-comma": "Eksporter til CSV (skilletegn: ,)",
|
||||
"export-board-csv-semicolon": "Eksporter til CSV (skilletegn: ;)",
|
||||
"export-board-tsv": "Eksporter tavle til TSV",
|
||||
"export-board-excel": "Eksporter tavle til Excel",
|
||||
"user-can-not-export-excel": "Brugeren kan ikke eksportere til Excel",
|
||||
"export-board-html": "Eksporter tavle til HTML",
|
||||
"export-card": "Eksporter kort",
|
||||
"export-card-pdf": "Eksporter kort til PDF",
|
||||
"export-card-excel": "Eksporter kort til Excel",
|
||||
"export-card-excel-fields": "Vælg felter at inkludere i Excel eksport",
|
||||
"export-card-subtasks": "Delopgaver",
|
||||
"export-card-attachments": "Vedhæftninger",
|
||||
"export-card-field-people": "Personer (Skapare, Ägare, Medlemmar, Tilldelade)",
|
||||
"export-card-field-board-info": "Tavelinformation (Tavla, Lista, Swimlane)",
|
||||
"export-card-field-dates": "Datum (Skapad, Mottagen, Start, Förfaller, Slut)",
|
||||
"export-card-attachment-filename": "Filnamn",
|
||||
"export-card-attachment-size": "Storlek",
|
||||
"export-card-attachment-type": "Skriv",
|
||||
"export-card-attachment-uploaded-by": "Uppladdat av",
|
||||
"export-card-attachment-uploaded-at": "Uppladdat den",
|
||||
"export-card-attachment-image-previews": "Förhandsvisning",
|
||||
"export-card-excel-no-disk-space": "Kan inte exportera till Excel: inte tillräckligt med ledigt diskutrymme",
|
||||
"export-card-field-people": "Personer (Opretter, Ejer, Medlemmer, Tildelte)",
|
||||
"export-card-field-board-info": "Tavleinformation (Tavle, Liste, Svømmebane)",
|
||||
"export-card-field-dates": "Datoer (Oprettet, Modtaget, Start, Forfald, Slut)",
|
||||
"export-card-attachment-filename": "Filnavn",
|
||||
"export-card-attachment-size": "Størrelse",
|
||||
"export-card-attachment-type": "Type",
|
||||
"export-card-attachment-uploaded-by": "Uploadet af",
|
||||
"export-card-attachment-uploaded-at": "Uploaded d.",
|
||||
"export-card-attachment-image-previews": "Forhåndsvisning",
|
||||
"export-card-excel-no-disk-space": "Kan ikke eksportere til Excel: Ikke nok ledig diskplads",
|
||||
"export-card-excel-free": "ledigt",
|
||||
"export-card-excel-needed": "krävs",
|
||||
"user-can-not-export-card-to-pdf": "Användaren kan inte exportera kort till PDF",
|
||||
"user-can-not-export-card-to-excel": "Användaren kan inte exportera kort till Excel",
|
||||
"exportBoardPopup-title": "Eksportér tavle",
|
||||
"exportCardPopup-title": "Exportera kort",
|
||||
"sort": "Sortér",
|
||||
"sorted": "Sorterad",
|
||||
"remove-sort": "Återställ sortering",
|
||||
"export-card-excel-needed": "påkrævet",
|
||||
"user-can-not-export-card-to-pdf": "Brugeren kan ikke eksportere kort til PDF",
|
||||
"user-can-not-export-card-to-excel": "Brugeren kan ikke eksportere kort til Excel",
|
||||
"exportBoardPopup-title": "Eksporter tavle",
|
||||
"exportCardPopup-title": "Eksporter kort",
|
||||
"sort": "Sorter",
|
||||
"sorted": "Sorteret",
|
||||
"remove-sort": "Fjern sortering",
|
||||
"sort-desc": "Klik for at sortere listen",
|
||||
"list-sort-by": "Sortér listen efter:",
|
||||
"list-sort-by": "Sorter listen efter:",
|
||||
"list-label-modifiedAt": "Senest tilgået:",
|
||||
"list-label-title": "Navn på listen",
|
||||
"list-label-sort": "Din manuelle ordre",
|
||||
"list-label-short-modifiedAt": "(S)",
|
||||
"list-label-short-title": "(N)",
|
||||
"list-label-short-sort": "(M)",
|
||||
"filter": "Filtrera",
|
||||
"filter-cards": "Filtrér kort eller lister",
|
||||
"filter-dates-label": "Filtrera efter datum",
|
||||
"filter-no-due-date": "Inget förfallodatum",
|
||||
"filter-overdue": "Försenad",
|
||||
"filter-due-today": "Förfaller idag",
|
||||
"filter-due-this-week": "Förfaller denna vecka",
|
||||
"filter-due-next-week": "Förfaller denna vecka",
|
||||
"filter-due-tomorrow": "Förfaller imorgon",
|
||||
"list-filter-label": "Filtrér lister efter titel",
|
||||
"filter-clear": "Ryd filter",
|
||||
"filter-labels-label": "Filtrera på etikett",
|
||||
"filter": "Filter",
|
||||
"filter-cards": "Filtrer kort eller lister",
|
||||
"filter-dates-label": "Filtrer efter dato",
|
||||
"filter-no-due-date": "Ingen forfaldsdato",
|
||||
"filter-overdue": "Forfaldsdato overskredet",
|
||||
"filter-due-today": "Forfalder i dag",
|
||||
"filter-due-this-week": "Forfalder i denne uge",
|
||||
"filter-due-next-week": "Forfalder i næste uge",
|
||||
"filter-due-tomorrow": "Forfalder i morgen",
|
||||
"list-filter-label": "Filtrer liste efter titel",
|
||||
"filter-clear": "Ryd filtrering",
|
||||
"filter-labels-label": "Filtrer på etikette",
|
||||
"filter-no-label": "Ingen etikette",
|
||||
"filter-member-label": "Filtrera på medlem",
|
||||
"filter-member-label": "Filtrer på medlem",
|
||||
"filter-no-member": "Ingen medlemmer",
|
||||
"filter-assignee-label": "Filtrera på tilldelad till",
|
||||
"filter-assignee-label": "Filtrer på tildelt til",
|
||||
"filter-no-assignee": "Utildelt",
|
||||
"filter-custom-fields-label": "Filtrera på anpassade fält",
|
||||
"filter-custom-fields-label": "Filtrer på brugerdefineret felt",
|
||||
"filter-no-custom-fields": "Ingen brugerdefinerede felter",
|
||||
"filter-show-archive": "Vis arkiverede lister",
|
||||
"filter-hide-empty": "Skjul tomme lister",
|
||||
"filter-on": "Filter er slået til",
|
||||
"filter-on-desc": "Du filtrerer kort på denne tavle. Klik her for at redigere filteret.",
|
||||
"filter-to-selection": "Filtrér til valgte",
|
||||
"other-filters-label": "Andra filter",
|
||||
"filter-to-selection": "Filtrer til valgte",
|
||||
"other-filters-label": "Andre filtre",
|
||||
"advanced-filter-label": "Avanceret filter",
|
||||
"advanced-filter-description": "Avanceret filter gør det muligt at skrive en tekststreng indeholdende følgende operatører: == != <= >= && || ( ) Mellemrum anvendes som adskillelsestegn mellem operatørerne. Du kan filtrere alle Brugerdefinerede felter ved at taste deres navne og værdier. Som eksempel: Felt1 == Værdi1. Bemærk: Hvis felter eller værdier indeholder mellemrum, så skal du indkapsle dem i enkeltcitationstegn. Som eksempel: 'Felt 1' == 'Værdi1'. For at springe over enkelte kontroltegn (' \\\\/), så kan \\\\ benyttes. Som eksempel: Felt1 == Så\\\\'n. Du kan også kombinere flere betingelser. Som eksempel: F1 == V1 || F1 == V2. Normalt vil alle operatører blive fortolket fra venstre mod højre. Du kan ændre rækkefølgen ved brug af parenteser. Som eksempel: F1 == V1 && (F2 == V2 || F2 == V3). Du kan også søge i tekstfelter med brug af regulære udtryk: F1 == /Tes.*/i",
|
||||
"fullname": "Fuldt navn",
|
||||
@@ -534,13 +534,13 @@
|
||||
"headerBarCreateBoardPopup-title": "Opret tavle",
|
||||
"home": "Hjem",
|
||||
"import": "Importér",
|
||||
"impersonate-user": "Imitera användare",
|
||||
"link": "Länk",
|
||||
"import-board": "importér tavle",
|
||||
"import-board-c": "Importér tavle",
|
||||
"import-board-title-trello": "Importér tavle fra Trello",
|
||||
"import-board-title-wekan": "Importér tavler fra tidligere eksport",
|
||||
"import-board-title-csv": "Importér tavle fra CSV/TSV",
|
||||
"impersonate-user": "Log ind som bruger",
|
||||
"link": "Link",
|
||||
"import-board": "importer tavle",
|
||||
"import-board-c": "Importer tavle",
|
||||
"import-board-title-trello": "Importer tavle fra Trello",
|
||||
"import-board-title-wekan": "Importer tavler fra tidligere eksport",
|
||||
"import-board-title-csv": "Importer tavle fra CSV/TSV",
|
||||
"from-trello": "Fra Trello",
|
||||
"from-wekan": "Fra forrige eksport",
|
||||
"from-csv": "Fra CSV/TSV",
|
||||
@@ -549,21 +549,64 @@
|
||||
"import-board-instruction-wekan": "På din tavle, gå til 'Menu', dernæst 'Eksportér tavle', og kopiér teksten i den hentede fil.",
|
||||
"import-board-instruction-about-errors": "Hvis du får fejl når der importeres en tavle, så vil importen undertiden stadig fungere, og tavlen vil være under side Alle tavler.",
|
||||
"import-json-placeholder": "Indsæt dine gyldige JSON-data her",
|
||||
"import-csv-placeholder": "Indsæt din valide CSV/TSV-data her",
|
||||
"import-map-members": "Kortlæg medlemmer",
|
||||
"import-csv-placeholder": "Indsæt dine gyldige CSV/TSV-data her",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map medlemmer",
|
||||
"import-members-map": "Dine importerede tavler rummer medlemmer. Kortlæg venligst de medlemmer du ønsker at importere til dine brugere.",
|
||||
"import-members-map-note": "Notera: Ommappade medlemmar kommer tilldelas den nuvarande användaren.",
|
||||
"import-show-user-mapping": "Gennemse kortlægning af medlemmer",
|
||||
"import-members-map-note": "Bemærk: Ikke-mappede medlemmer vil blive mappet til den nuværende bruger.",
|
||||
"import-show-user-mapping": "Gennemse mapning af medlemmer",
|
||||
"import-user-select": "Vælg din eksisterende bruger, som du ønsker at bruge for dette medlem",
|
||||
"importMapMembersAddPopup-title": "Vælg medlem",
|
||||
"info": "Versjon",
|
||||
"info": "Version",
|
||||
"initials": "Initialer",
|
||||
"invalid-date": "Ugyldig dato",
|
||||
"invalid-time": "Ugyldig tidsangivelse",
|
||||
"invalid-user": "Ugyldig bruger",
|
||||
"joined": "sluttede sig til",
|
||||
"joined": "sammenlagt",
|
||||
"just-invited": "Du er netop blevet inviteret til denne tavle",
|
||||
"keyboard-shortcuts": "Tastaturgenveje",
|
||||
"keyboard-shortcuts": "Genvejstaster",
|
||||
"label-create": "Opret etikette",
|
||||
"label-default": "%s etikette (standard)",
|
||||
"label-delete-pop": "Det er ikke muligt at fortryde. Dette vil fjerne etiketten fra alle kort og ødelægge dets historik.",
|
||||
@@ -580,9 +623,9 @@
|
||||
"list-select-cards": "Vælg alle kort i denne liste",
|
||||
"set-color-list": "Angiv farve",
|
||||
"listActionPopup-title": "Handlinger for liste",
|
||||
"settingsUserPopup-title": "Användarinställningar",
|
||||
"settingsTeamPopup-title": "Team inställningar",
|
||||
"settingsOrgPopup-title": "Organisationens inställningar",
|
||||
"settingsUserPopup-title": "Brugerindstillinger",
|
||||
"settingsTeamPopup-title": "Holdindstillinger",
|
||||
"settingsOrgPopup-title": "Organisationsindstillinger",
|
||||
"swimlaneActionPopup-title": "Handlinger for svømmebane",
|
||||
"swimlaneAddPopup-title": "Tilføj en svømmebane nedenfor",
|
||||
"listImportCardPopup-title": "Importér et Trello-kort",
|
||||
@@ -601,18 +644,18 @@
|
||||
"memberMenuPopup-title": "Medlemsindstillinger",
|
||||
"grey-icons": "Grå ikoner",
|
||||
"members": "Medlemmer",
|
||||
"menu": "Meny",
|
||||
"menu": "Menu",
|
||||
"move-selection": "Flyt valgte",
|
||||
"moveCardPopup-title": "Flyt kort",
|
||||
"moveListPopup-title": "Flytta lista",
|
||||
"moveCardToBottom-title": "Flyt til bunden",
|
||||
"moveCardToTop-title": "Flyt til toppen",
|
||||
"moveSelectionPopup-title": "Flyt valgte",
|
||||
"copySelectionPopup-title": "Kopiera markering",
|
||||
"selection-color": "Markeringsfärg",
|
||||
"copySelectionPopup-title": "Kopier markering",
|
||||
"selection-color": "Markeringsfarve",
|
||||
"multi-selection": "Multivalg",
|
||||
"multi-selection-label": "Ange etikett för val",
|
||||
"multi-selection-member": "Ange medlem för val",
|
||||
"multi-selection-label": "Angiv etikette for valg",
|
||||
"multi-selection-member": "Angiv medlem for valg",
|
||||
"multi-selection-on": "Multivalg er slået til",
|
||||
"muted": "Slukket",
|
||||
"muted-info": "Du vil aldrig få notifikationer om ændringer i denne tavle",
|
||||
@@ -624,10 +667,10 @@
|
||||
"no-results": "Ingen resultater",
|
||||
"normal": "Normal",
|
||||
"normal-desc": "Du kan se og redigere kort. Indstillinger kan ikke ændres.",
|
||||
"normal-assigned-only": "Endast tilldelad normal",
|
||||
"normal-assigned-only-desc": " Endast tilldelade kort synliga. Redigera som normal användare.",
|
||||
"normal-assigned-only": "Kun tildelt normal",
|
||||
"normal-assigned-only-desc": "Kun tildelte kort synlige. Rediger som normal bruger.",
|
||||
"not-accepted-yet": "Invitation er endnu ikke accepteret",
|
||||
"notify-participate": "Få uppdateringar på alla kort du är med som deltagare, skapare eller medlem",
|
||||
"notify-participate": "Modtag opdateringer på alle kort, som du deltager i som opretter eller medlem",
|
||||
"notify-watch": "Modtag opdateringer for alle tavler eller kort som du følger",
|
||||
"optional": "valgfri",
|
||||
"or": "eller",
|
||||
@@ -645,29 +688,29 @@
|
||||
"public": "Offentlig",
|
||||
"public-desc": "Denne tavle er offentlig. Den er synlig for alle med linket og vil blive vist i søgemaskiner som Google. Det er kun personer tilføjet til tavlen, der kan redigere.",
|
||||
"quick-access-description": "Stjernemarkér en tavle for at tilføje genvej i denne bjælke.",
|
||||
"remove-cover": "Ta bort omslagsbild för minikort",
|
||||
"remove-cover": "Fjern omslagsbillede for minikort",
|
||||
"remove-from-board": "Fjern fra tavle",
|
||||
"remove-label": "Fjern etikette",
|
||||
"listDeletePopup-title": "Slet liste?",
|
||||
"remove-member": "Fjern medlem",
|
||||
"remove-member-from-card": "Fjern fra kort",
|
||||
"remove-member-pop": "Fjern __name__ (__username__) fra __boardTitle__? Medlemmet vil blive fjernetfra alle kort på denne tavle. De vil modtage en notifikation.",
|
||||
"remove-member-pop": "Fjern __name__ (__username__) fra __boardTitle__? Medlemmet vil blive fjernet fra alle kort på denne tavle. De vil modtage en notifikation.",
|
||||
"removeMemberPopup-title": "Fjern medlem?",
|
||||
"rename": "Omdøb",
|
||||
"rename-board": "Omdøb tavle",
|
||||
"restore": "Genskab",
|
||||
"rescue-card-description": "Visa räddningsdialogruta innan stängning för osparade kortbeskrivningar",
|
||||
"rescue-card-description-dialogue": "Skriv över kortbeskrivningen med dina ändringar?",
|
||||
"rescue-card-description": "Vis redningsdialog inden lukning af ikke-gemte kort beskrivelser ",
|
||||
"rescue-card-description-dialogue": "Overskriv nuværende kort beskrivelse med dine ændringer?",
|
||||
"save": "Gem",
|
||||
"search": "Søg",
|
||||
"rules": "Regler",
|
||||
"search-cards": "Søg ud fra titler i kort/lister, beskrivelser og brugerdefinerede felter på denne tavle.",
|
||||
"search-example": "Text att söka efter?",
|
||||
"search-example": "Skriv den tekst, som du søger efter og tryk på Enter",
|
||||
"select-color": "Vælg farve",
|
||||
"select-board": "Välj tavla",
|
||||
"select-board": "Vælg tavle",
|
||||
"set-wip-limit-value": "Angiv en grænse for det maksimale antal opgaver i denne liste",
|
||||
"setWipLimitPopup-title": "Angiv WIP-begrænsning",
|
||||
"shortcut-add-self": "Tilldela dig nuvarande kort",
|
||||
"shortcut-add-self": "Tildel dig selv til nuværende kort",
|
||||
"shortcut-assign-self": "Tilknyt dig selv til nuværende kort?",
|
||||
"shortcut-autocomplete-emoji": "Auto-fuldfør emoji",
|
||||
"shortcut-autocomplete-members": "Auto-fuldfør medlemmer",
|
||||
@@ -697,23 +740,23 @@
|
||||
"has-spenttime-cards": "Har kort med anvendt tid",
|
||||
"time": "Tid",
|
||||
"title": "Titel",
|
||||
"toggle-assignees": "Växla tilldelade personer 1-9 för kort (I den ordning de lades till i tavlan).",
|
||||
"toggle-labels": "Toggle-etiketter 1-9 för kortet. Multi-Selection lägger till etiketter 1-9.",
|
||||
"remove-labels-multiselect": "Multi-Selection raderar etikett 1-9",
|
||||
"toggle-assignees": "Skift tildelte personer 1-9 for kort (i den rækkefølge de blev tilføjet til tavlen).",
|
||||
"toggle-labels": "Skift etiketter 1-9 for kortet. Multi-Selection lægger etiketterne 1-9 til",
|
||||
"remove-labels-multiselect": "Multi-Selection sletter etiketterne 1-9",
|
||||
"tracking": "Sporing",
|
||||
"tracking-info": "Du vil få notifikation om alle ændringer i kort som du har oprettet eller er medlem af.",
|
||||
"type": "Skriv",
|
||||
"unassign-member": "Fjern medlemstilknytning",
|
||||
"unsaved-description": "Du har en beskrivelse som ikke er gemt",
|
||||
"unwatch": "Ophør med at følge",
|
||||
"unwatch": "Stop med at følge",
|
||||
"upload": "Overfør",
|
||||
"upload-avatar": "Overfør en avatar",
|
||||
"uploaded-avatar": "Overførte en avatar",
|
||||
"uploading-files": "Laddar upp filer",
|
||||
"upload-failed": "Uppladdning misslyckades",
|
||||
"upload-completed": "Uppladdning slutförd",
|
||||
"custom-top-left-corner-logo-image-url": "Länk till anpassad logotypbild",
|
||||
"custom-top-left-corner-logo-link-url": "Länk för anpassad logotyp",
|
||||
"uploading-files": "Uploader filer",
|
||||
"upload-failed": "Uploading mislykkedes",
|
||||
"upload-completed": "Uploading fuldført",
|
||||
"custom-top-left-corner-logo-image-url": "URL til brugerdefineret logobillede øverst til venstre",
|
||||
"custom-top-left-corner-logo-link-url": "URL til brugerdefineret logolink øverst til venstre",
|
||||
"custom-top-left-corner-logo-height": "Anpassad logotyphöjd uppe till vänster. Standard: 27",
|
||||
"custom-login-logo-image-url": "Länk till bild för anpassad logotyp på inloggningssidan",
|
||||
"custom-login-logo-link-url": "Länk för anpassad logotyp på inloggningssidan",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sortering er slået til",
|
||||
"cardsSortPopup-title": "Sortera kort",
|
||||
"due-date": "Förfallodatum",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Serverfel",
|
||||
"server-error-troubleshooting": "Skicka in felet som inträffade på servern.\nFör snap-installationer, kör: `sudo snap logs wekan.wekan`\nFör Docker-installationer, kör: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Titel (Alfabetisk)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Vedhæftninger",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Skriv",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Misslyckades med att stoppa migreringar",
|
||||
"migration-stopped": "Migreringar har stoppats",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS-lagring",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pausa alla migreringar",
|
||||
"s3-access-key": "S3-åtkomstnyckel",
|
||||
"s3-access-key-description": "AWS S3-åtkomstnyckel för autentisering",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
|
||||
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
|
||||
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mitglieder zuordnen",
|
||||
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
|
||||
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sortierung ist aktiviert",
|
||||
"cardsSortPopup-title": "Sortiere Karten",
|
||||
"due-date": "Fälligkeitsdatum",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server-Fehler",
|
||||
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Überschrift (alphabetisch)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Anhänge",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Typ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
|
||||
"migration-stopped": "Migrationen erfolgreich gestoppt",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Alle Migrationen anhalten",
|
||||
"s3-access-key": "S3 Zugriffsschlüssel",
|
||||
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
|
||||
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
|
||||
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mitglieder zuordnen",
|
||||
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
|
||||
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sortierung ist aktiviert",
|
||||
"cardsSortPopup-title": "Sortiere Karten",
|
||||
"due-date": "Fälligkeitsdatum",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server-Fehler",
|
||||
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Überschrift (alphabetisch)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Anhänge",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Typ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
|
||||
"migration-stopped": "Migrationen erfolgreich gestoppt",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Alle Migrationen anhalten",
|
||||
"s3-access-key": "S3 Zugriffsschlüssel",
|
||||
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
"error-board-doesNotExist": "Dieses Board existiert nicht",
|
||||
"error-board-notAdmin": "Um das zu tun, müssen Sie Administrator dieses Boards sein",
|
||||
"error-board-notAMember": "Um das zu tun, müssen Sie Mitglied dieses Boards sein",
|
||||
"error-notAllowed": "Your board role is not allowed to do that",
|
||||
"error-notAllowed": "Ihre Board Rolle lässt diese Aktion nicht zu",
|
||||
"error-json-malformed": "Ihre Eingabe ist kein gültiges JSON",
|
||||
"error-json-schema": "Ihre JSON-Daten enthalten nicht die gewünschten Informationen im richtigen Format",
|
||||
"error-csv-schema": "Deine CSV(Kommatgetrennte Werte)/TSV (Tab getrennte Werte) enthält keine korrekten Informationen im geforderten Format ",
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
|
||||
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
|
||||
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mitglieder zuordnen",
|
||||
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
|
||||
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
|
||||
@@ -741,7 +784,7 @@
|
||||
"wipLimitErrorPopup-dialog-pt2": "Bitte verschieben Sie einige Aufgaben aus dieser Liste oder setzen Sie ein grösseres WIP-Limit.",
|
||||
"admin-panel": "Administration",
|
||||
"settings": "Einstellungen",
|
||||
"attachment-transfer-limits-title": "Attachment And API File Size Limits",
|
||||
"attachment-transfer-limits-title": "Anhang und API Dateigrößen Limits",
|
||||
"attachment-limits": "Limits",
|
||||
"attachment-transfer-limits-description": "Configure maximum file size limits for uploads and downloads. API transfer buffering has separate server-side safety caps.",
|
||||
"attachment-transfer-limits-saved": "Attachment transfer limits saved.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sortierung ist aktiviert",
|
||||
"cardsSortPopup-title": "Sortiere Karten",
|
||||
"due-date": "Fälligkeitsdatum",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server-Fehler",
|
||||
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Überschrift (alphabetisch)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Anhänge",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Typ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
|
||||
"migration-stopped": "Migrationen erfolgreich gestoppt",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Alle Migrationen anhalten",
|
||||
"s3-access-key": "S3 Zugriffsschlüssel",
|
||||
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Treten beim importieren eines Board Fehler auf, so kann der Import dennoch erfolgreich abgeschlossen sein und das Board ist auf der Seite \"Alle Boards\" zusehen.",
|
||||
"import-json-placeholder": "Fügen Sie die korrekten JSON-Daten hier ein",
|
||||
"import-csv-placeholder": "Fügen Sie die korrekten CSV/TSV-Daten hier ein",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Mitglieder zuordnen",
|
||||
"import-members-map": "Das importierte Board hat einige Mitglieder. Bitte ordnen sie die Mitglieder, die Sie importieren wollen, Ihren Benutzern zu.",
|
||||
"import-members-map-note": "Anmerkung: Nicht zugeordnete Mitglieder werden dem aktuellen Benutzer zugeordnet.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sortierung ist aktiviert",
|
||||
"cardsSortPopup-title": "Sortiere Karten",
|
||||
"due-date": "Fälligkeitsdatum",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server-Fehler",
|
||||
"server-error-troubleshooting": "Bitte übermitteln Sie den Fehler, den der Server erzeugt hat.\nRufen Sie für eine Snap-Installation auf: `sudo snap logs wekan.wekan`\nRufen Sie für eine Docker-Installation auf: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Überschrift (alphabetisch)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Anhänge",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Typ",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Stoppen der Migrationen fehlgeschlagen",
|
||||
"migration-stopped": "Migrationen erfolgreich gestoppt",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Speicher",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Alle Migrationen anhalten",
|
||||
"s3-access-key": "S3 Zugriffsschlüssel",
|
||||
"s3-access-key-description": "AWS S3 Zugangsschlüssel zur Authentifizierung",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Se hai degli errori quando importi una bacheca, qualche volta l'importazione funziona comunque, e la bacheca si trova nella pagina \"Tutte le bacheche\"",
|
||||
"import-json-placeholder": "Επικολλήστε τα ορθά JSON δεδομένα σας εδώ",
|
||||
"import-csv-placeholder": "Επικολλήστε τα ορθά CSV/TSV δεδομένα σας εδώ",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Αντιστοίχιση μελών",
|
||||
"import-members-map": "Ο εισαχθείς πίνακας έχει κάποια μέλη. Παρακαλούμε αντιστοιχίστε τα μέλη που θέλετε να εισάγετε στους χρήστες σας",
|
||||
"import-members-map-note": "Nota: I membri non mappati verranno assegnati all'utente corrente.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Ordinamento attivo",
|
||||
"cardsSortPopup-title": "Ordina schede",
|
||||
"due-date": "Data di scadenza",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Errore server",
|
||||
"server-error-troubleshooting": "Merci de soumettre l'erreur générée par le serveur. Pour une installation snap, lancer `sudo snap logs wekan.wekan`. Pour une installation docker, lancer `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Titolo (ordine alfabetico)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Συνημμένα",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Τύπος",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "Se hai degli errori quando importi una bacheca, qualche volta l'importazione funziona comunque, e la bacheca si trova nella pagina \"Tutte le bacheche\"",
|
||||
"import-json-placeholder": "Επικολλήστε τα ορθά JSON δεδομένα σας εδώ",
|
||||
"import-csv-placeholder": "Επικολλήστε τα ορθά CSV/TSV δεδομένα σας εδώ",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Αντιστοίχιση μελών",
|
||||
"import-members-map": "Ο εισαχθείς πίνακας έχει κάποια μέλη. Παρακαλούμε αντιστοιχίστε τα μέλη που θέλετε να εισάγετε στους χρήστες σας",
|
||||
"import-members-map-note": "Nota: I membri non mappati verranno assegnati all'utente corrente.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Ordinamento attivo",
|
||||
"cardsSortPopup-title": "Ordina schede",
|
||||
"due-date": "Data di scadenza",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Errore server",
|
||||
"server-error-troubleshooting": "Merci de soumettre l'erreur générée par le serveur. Pour une installation snap, lancer `sudo snap logs wekan.wekan`. Pour une installation docker, lancer `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Titolo (ordine alfabetico)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Συνημμένα",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Τύπος",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
@@ -550,6 +550,49 @@
|
||||
"import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.",
|
||||
"import-json-placeholder": "Paste your valid JSON data here",
|
||||
"import-csv-placeholder": "Paste your valid CSV/TSV data here",
|
||||
"import-json-file": "Or select a Trello JSON file",
|
||||
"import-attachments-zip": "Trello attachments ZIP (optional, from Trello Card Attachments Downloader)",
|
||||
"import-trello-json-file": "Trello .json file",
|
||||
"import-trello-json-file-hint": "One Trello board exported as .json. If you have saved Trello API key and token, its attachments, board background and member avatars are downloaded too.",
|
||||
"import-trello-zip-file": "Trello .zip file",
|
||||
"import-trello-zip-file-hint": "A .zip with one or more board .json files and their attachments in board-name subfolders (from the Trello Card Attachments Downloader). All boards are imported.",
|
||||
"import-trello-zip-no-boards": "No Trello board .json files were found in the .zip.",
|
||||
"import-trello-zip-progress": "Importing boards from .zip, please wait…",
|
||||
"import-trello-failed": "Importing from Trello failed.",
|
||||
"import-trello-zip-failed": "Importing the .zip failed.",
|
||||
"import-trello-zip-read-failed": "Could not read the uploaded .zip.",
|
||||
"import-trello-zip-too-large": "The .zip is too large to import.",
|
||||
"import-trello-zip-too-many-files": "The .zip has too many files to import.",
|
||||
"import-trello-zip-file-too-large": "A file inside the .zip is too large to import.",
|
||||
"import-trello-zip-unsafe-path": "The .zip contains unsafe file paths and was rejected.",
|
||||
"import-trello-workspace": "Personal workspace name (optional)",
|
||||
"import-trello-workspace-placeholder": "Imported board is placed under this workspace, created if it does not exist",
|
||||
"import-trello-parent-workspace": "Place imported workspaces under",
|
||||
"trello-api-import": "Import directly from Trello with API key and token",
|
||||
"trello-api-import-desc": "Paste your Trello API key and token to list your Trello workspaces and boards, then import the selected ones with their attachments.",
|
||||
"trello-api-key": "Trello API key (from https://trello.com/app-key)",
|
||||
"trello-api-token": "Trello API token (manually generated below the API key)",
|
||||
"trello-list-workspaces": "List Trello workspaces",
|
||||
"trello-parent-workspace-top": "Top level",
|
||||
"trello-import-selected": "Import selected boards",
|
||||
"trello-importing": "Importing boards…",
|
||||
"trello-import-results": "Import results",
|
||||
"trello-api-credentials-required": "Please enter both the Trello API key and token.",
|
||||
"trello-api-credentials-saved": "Trello API key and token are saved. You can list workspaces without re-entering them.", "trello-select-boards": "Please select at least one board to import.",
|
||||
"select-all": "Select all",
|
||||
"unselect-all": "Unselect all",
|
||||
"trello-import-more": "Import more boards",
|
||||
"trello-import-progress": "Import progress",
|
||||
"trello-cancel": "Cancel import",
|
||||
"trello-cancel-delete": "Cancel and delete imported",
|
||||
"trello-cancel-delete-confirm": "Cancel the import and delete the boards already imported by this job? This cannot be undone.",
|
||||
"trello-resume": "Resume import",
|
||||
"trello-delete-imported": "Delete imported boards",
|
||||
"trello-clear-job": "Clear",
|
||||
"trello-import-errors": "Import errors",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"import-map-members": "Map members",
|
||||
"import-members-map": "Your imported board has some members. Please map the members you want to import to your users",
|
||||
"import-members-map-note": "Note: Unmapped members will be assigned to the current user.",
|
||||
@@ -1261,6 +1304,32 @@
|
||||
"sort-is-on": "Sort is on",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"due-complete": "Due complete",
|
||||
"card-mark-complete": "Mark as complete",
|
||||
"card-mark-incomplete": "Mark as incomplete",
|
||||
"stickers": "Stickers",
|
||||
"add-sticker": "Add sticker",
|
||||
"remove-sticker": "Remove sticker",
|
||||
"board-backgrounds": "Board backgrounds",
|
||||
"boardBackgrounds-title": "Board backgrounds",
|
||||
"upload-background": "Upload background image",
|
||||
"set-as-active": "Set as board background",
|
||||
"background-too-big": "Background image is too big (Maximum {{size}}).",
|
||||
"board-background-delete-pop": "Are you sure you want to delete this background image?",
|
||||
"location": "Location",
|
||||
"location-open-map": "Open in map",
|
||||
"add-location": "Add location",
|
||||
"cardLocationsPopup-title": "Add location",
|
||||
"location-name": "Location name",
|
||||
"location-address": "Address",
|
||||
"location-latitude": "Latitude",
|
||||
"location-longitude": "Longitude",
|
||||
"location-detect-from-map": "Detect location from any map link",
|
||||
"location-detect": "Detect",
|
||||
"location-detect-none": "Could not detect a location from this link.",
|
||||
"location-detect-done": "Filled in the detected location details below.",
|
||||
"location-open-map-at": "Open map links at",
|
||||
"map-provider-saved": "Default map service saved.",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
@@ -1377,6 +1446,20 @@
|
||||
"move-storage-fs": "Filesystem",
|
||||
"move-storage-s3": "S3/MinIO",
|
||||
"move-all-attachments": "Move all attachments",
|
||||
"move-attachments-none-found": "Nothing is stored on the selected source storage, so there is nothing to move.",
|
||||
"attachment-last-move": "Last move",
|
||||
"attachment-repair-locations": "Repair file locations",
|
||||
"attachment-repair-locations-description": "Find attachments and avatars whose recorded storage (filesystem / GridFS / cloud) no longer matches where the file is, and fix the database to point at the real location.",
|
||||
"attachment-repair-running": "Searching and repairing…",
|
||||
"attachment-repair-done": "Repair finished.",
|
||||
"attachment-repair-scanned": "Searched",
|
||||
"attachment-repair-repaired": "Repaired",
|
||||
"attachment-repair-broken": "Not found",
|
||||
"move-scope": "Move",
|
||||
"move-scope-attachments": "Attachments",
|
||||
"move-scope-avatars": "Avatars",
|
||||
"move-scope-both": "Attachments and Avatars",
|
||||
"move-storage-all": "All Read-enabled storages",
|
||||
"default-save-storage": "Default Save Storage",
|
||||
"default-save-storage-description": "Choose the storage backend where newly uploaded attachments are saved.",
|
||||
"default-save-storage-saved": "Default save storage saved",
|
||||
@@ -1389,7 +1472,7 @@
|
||||
"calculating-counts": "Calculating counts...",
|
||||
"stats-scope": "Type",
|
||||
"stats-collectionfs": "CollectionFS",
|
||||
"stats-mongo-files": "Mongo-Files",
|
||||
"stats-mongo-files": "Meteor-Files",
|
||||
"stats-count": "Count",
|
||||
"avatars": "Avatars",
|
||||
"card-id": "Card ID",
|
||||
@@ -1555,7 +1638,25 @@
|
||||
"gcs-key-filename-description": "Optional: path to a service account JSON key file on the server.",
|
||||
"gcs-credentials": "Service account credentials (JSON)",
|
||||
"gcs-credentials-description": "Optional: paste the service account JSON key. Leave blank to use the key file or default credentials.",
|
||||
"gcs-permissions-note": "Give WeKan's service account read/write access to the bucket: Google Cloud Console → Cloud Storage → Buckets → your bucket → Permissions → Grant access. In \"New principals\", paste the service account email — the \"client_email\" value from your JSON key — then choose the role \"Storage Object Admin\" and Save.",
|
||||
"cloud-secret-keep-blank": "Leave blank to keep the current value.",
|
||||
"azure-account-name-description": "The name of your Azure Storage account.",
|
||||
"azure-container-description": "The blob container to store files in.",
|
||||
"gcs-project-id-description": "Your Google Cloud project id.",
|
||||
"gcs-bucket-description": "The name of the Cloud Storage bucket to store files in.",
|
||||
"s3-endpoint-menu-path": "AWS: leave blank (uses the region endpoint). S3-compatible: the Endpoint URL from your MinIO / Cloudflare R2 / Backblaze B2 / Wasabi / DigitalOcean Spaces console.",
|
||||
"s3-region-menu-path": "AWS Console → S3 → Buckets → your bucket → Properties → AWS Region.",
|
||||
"s3-bucket-menu-path": "AWS Console → S3 → Buckets → your bucket name (or Create bucket → Bucket name).",
|
||||
"s3-access-key-menu-path": "AWS Console → IAM → Users → your user → Security credentials → Access keys → Create access key → choose use case 'Application running outside AWS' → Create access key. Shown as 'Access key ID'.",
|
||||
"s3-secret-key-menu-path": "Same screen as the Access key ID; 'Secret access key' is shown only once when the key is created — copy it then (or Download .csv).",
|
||||
"azure-account-name-menu-path": "Azure Portal → Storage accounts → your account → name shown at the top (also under Security + networking → Access keys → Storage account name).",
|
||||
"azure-account-key-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Show → Key.",
|
||||
"azure-connection-string-menu-path": "Azure Portal → Storage accounts → your account → Security + networking → Access keys → key1 → Connection string → Show.",
|
||||
"azure-container-menu-path": "Azure Portal → Storage accounts → your account → Data storage → Containers → your container (or + Container to create one).",
|
||||
"gcs-project-id-menu-path": "Google Cloud Console → project selector at the top → your project's ID (Cloud overview → Dashboard → Project info → Project ID).",
|
||||
"gcs-bucket-menu-path": "Google Cloud Console → Cloud Storage → Buckets → your bucket name (or Create to make one).",
|
||||
"gcs-key-filename-menu-path": "Path on the WeKan server to the downloaded service account JSON key file (created below). Use this OR paste the JSON into the field below.",
|
||||
"gcs-credentials-menu-path": "Google Cloud Console → IAM & Admin → Service accounts → your service account → Keys → Add key → Create new key → JSON → Create (downloads the file). Paste its contents here, or save the file and set its path above.",
|
||||
"cloud-secret-set": "(saved — leave blank to keep)",
|
||||
"cloud-secret-none": "(not set)",
|
||||
"test-cloud-connection": "Test connection",
|
||||
@@ -1590,6 +1691,7 @@
|
||||
"migration-stop-failed": "Failed to stop migrations",
|
||||
"migration-stopped": "Migrations stopped successfully",
|
||||
"mongodb-gridfs-storage": "MongoDB GridFS Storage",
|
||||
"gridfs-move-collectionfs-note": "To have attachments and avatars visible, move them from CollectionFS to any other Storage.",
|
||||
"pause-all-migrations": "Pause All Migrations",
|
||||
"s3-access-key": "S3 Access Key",
|
||||
"s3-access-key-description": "AWS S3 access key for authentication",
|
||||
|
||||
Loaded 100 of 337 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user