diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e7602f898..1e064458e 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -2,35 +2,46 @@ name: Code Quality on: push: - # An ALLOW-LIST of branch prefixes is a gate with a hole in it, and the - # hole is SILENT: a branch matching nothing gets no CI at all, and its last - # visible status is whatever it inherited — indistinguishable, on every - # dashboard, from a branch that passed. + # DEFAULT BRANCHES ONLY. `pull_request` below carries every other branch. # - # Two live examples, both found 2026-08-14: `perf/**` was uncovered in - # openconnector, where a merge carrying unresolved conflict markers and 84 - # failing tests was pushed and nothing ran; and `feat/**` was uncovered in - # openregister — note the list said `feature/**`, so every branch anyone - # named `feat/...` had been running unchecked. + # This was an allow-list of branch prefixes, and that was a gate with a + # SILENT hole: a branch matching nothing got no CI at all, and its last + # visible status was whatever it inherited — indistinguishable, on every + # dashboard, from a branch that passed. Two live examples, both found + # 2026-08-14: `perf/**` was uncovered in openconnector, where a merge + # carrying unresolved conflict markers and 84 failing tests was pushed and + # nothing ran; and `feat/**` was uncovered in openregister, because the + # list said `feature/**`. # - # Prefixes are added rather than replaced with `**` because this workflow is - # expensive (PHPUnit matrix, Newman, Playwright). The fast structural checks - # DO run on `**` — see merge-hygiene.yml, added in the same change. + # The comment that stood here said adding prefixes was not the durable fix, + # and that the durable fix was to let the pull_request trigger gate it. + # THIS IS THAT CHANGE. # - # ⚠️ Adding prefixes is not the durable fix; the next invented one is - # uncovered again. The durable fix is branch protection requiring a PR into - # development, which the pull_request trigger below already gates correctly. + # What forced it now: a push to a branch with an open PR ran the SAME 34 + # jobs TWICE on the same commit. `concurrency` cannot dedupe them — the + # group is suffixed by event name deliberately (.github#540: a + # default-branch push carries jobs a PR run does not, and a dispatch must + # not be cancellable by a standing release PR), so the two events sit in + # different lanes BY DESIGN and both run to completion. Measured fleet-wide + # 2026-08-25..27, 659 of 2,106 Code Quality runs were that duplicate — 31% + # of the fleet's most expensive workflow, re-deciding a commit another run + # was already deciding. The account ceiling is 60 concurrent jobs (Team + # plan); the fleet was measured at 53 running with 1,528 jobs queued behind + # them, the oldest run 7 hours old and not yet started. + # + # NO BRANCH LOSES ITS FLOOR. merge-hygiene.yml runs on `'**'` — every + # branch anyone pushes, no prefix list to forget — and it is the check + # `development` actually requires. That is the smoke alarm; this workflow + # is the fire brigade and belongs on the PR. Of 668 feature-branch push + # runs in that window, only NINE were on a branch with no PR run beside + # them. + # + # The default branches STAY: their push runs are not duplicates, they are + # the only carrier of Coverage Baseline Check, SBOM and Features Extract, + # none of which run on a pull_request event. branches: - main - development - - feature/** - - feat/** - - bugfix/** - - hotfix/** - - perf/** - - refactor/** - - chore/** - - fix/** pull_request: branches: [main, master, development, beta] workflow_dispatch: @@ -103,7 +114,20 @@ concurrency: # A branch name is not a unique lane when two event types can each produce a # run for it, so the event is now always part of the key. group: quality-${{ github.head_ref || github.ref_name }}${{ github.event_name != 'pull_request' && format('-{0}', github.event_name) || '' }} - cancel-in-progress: true + + # PUSH RUNS ARE NOT CANCELLED — and this has to be said HERE, not only in the + # shared workflow. .github#597 set `cancel-in-progress` on quality.yml itself, + # but a caller's own concurrency cancels the whole run before the called + # workflow's setting can apply, so that fix reached only the apps that declare + # no concurrency of their own. Measured 2026-08-28 over push runs on + # `development` since #597: 0 of 11 cancelled where the caller was silent, 7 of + # 13 (54%) cancelled where the caller still said `true`. + # + # An integration branch needs a verdict per commit: the run being cancelled is + # the only thing that would have said whether what just landed is sound, and + # its replacement is cancelled too. `pull_request` keeps cancelling, where + # superseding really is correct. + cancel-in-progress: ${{ github.event_name != 'push' }} # Permission CEILING for the called quality pipeline. GitHub statically # validates the called workflow's declared job permissions against this diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index ffa8352d8..1aa4c05fc 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,5 +1,11 @@ name: Documentation +# Publishes the docs site to the Cloudflare Worker that serves it. +# +# TRIGGERS ON `development`, NOT ON A `documentation` BRANCH. This file used to +# listen on a branch called `documentation`; nobody has pushed to one since +# 2026-05-25, so the site simply stopped being rebuilt while every docs change +# merged to development satisfied its review and published nothing. on: push: branches: [development] @@ -9,48 +15,30 @@ on: jobs: deploy: uses: ConductionNL/.github/.github/workflows/documentation.yml@main - # A reusable workflow receives NO secrets by default. Without this block - # `secrets.CF_API_TOKEN` is empty inside the callee, its "Publish to the - # Cloudflare Worker" step skips itself on its own guard, and the run - # finishes GREEN having written only gh-pages — which nothing serves. The - # live site never changes and no check goes red to say so. - # - # Mapped explicitly rather than `secrets: inherit`, because `inherit` - # hands the callee EVERY secret this repo holds — signing cert and key, - # appstore token, deploy keys — for the sake of two Cloudflare values. - # This way only those two cross the boundary. - # - # The exposure above is the ONLY reason for the explicit mapping. The - # names are the same on both sides: the org secrets really are - # `CF_API_TOKEN` / `CF_ACCOUNT_ID` — the names ConductionNL/.github's own - # deploy-docs.yml reads directly, and the names the callee declares under - # `workflow_call.secrets`. - # - # This block used to read `secrets.CLOUDFLARE_API_TOKEN` / - # `secrets.CLOUDFLARE_ACCOUNT_ID`, which are not secrets anywhere in this - # org. Mapping from a name that does not exist is NOT an error — it - # yields an empty string — so the callee's publish step skipped itself on - # its own guard and the run still finished green. Measured on planninq - # run 32760529026: "Publish to the Cloudflare Worker" SKIPPED, the log - # showing `CF_API_TOKEN:` with no value. + # A reusable workflow receives NO secrets by default. Without this block the + # callee's publish step finds CF_API_TOKEN empty, skips itself on its own + # `if:` guard, and the run finishes GREEN having changed nothing -- the + # failure that left the fleet's docs sites on May builds. The names are the + # same on both sides; the org secrets really are CF_API_TOKEN/CF_ACCOUNT_ID. secrets: CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} with: - # `keepiq.conduction.nl` resolves as of 2026-08-23 — attached as a second - # custom domain on the SAME `doriath-docs` worker that serves - # `doriath.conduction.nl`. Both hosts answer, so nothing goes dark either - # way. docs-hosts must list BOTH: wrangler reconciles the worker's - # triggers against it, so a host omitted there is removed. Keep this in - # step with docs/static/CNAME. cname: keepiq.conduction.nl + # doriath.conduction.nl is the retired hostname. It stays in docs-hosts so + # existing links keep resolving, and canonical-host below turns it into a + # 301 rather than a second live copy of every page. + # EVERY host this worker answers on, in FULL: wrangler reconciles the + # worker's triggers against this list, so a host left out is REMOVED and + # goes dark. docs-hosts: doriath.conduction.nl,keepiq.conduction.nl - - # Named explicitly, because the comment above already knows the answer - # and the workflow did not. The callee derives the worker from `cname` - # when not told — `keepiq-docs`, which does not exist. Deploying that - # creates a SECOND worker while both custom domains keep routing to - # `doriath-docs`: every deploy green, reaching nobody. Renaming the - # worker is a Cloudflare-side move, not something this file can perform. + # The ONE hostname this site is reached on. Every other host in + # docs-hosts answers 301 to the same path here. Before this, both hostnames + # served identical content and the retired name stayed as discoverable + # as the current one. + canonical-host: keepiq.conduction.nl + # PINNED. Deriving the name is how a deploy goes green and reaches + # nobody: wrangler creates the derived worker and publishes there while + # the custom domains keep routing to the real one. worker-name: doriath-docs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b32036b0c..bf12f102c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,12 @@ concurrency: group: release-${{ github.ref_name }} cancel-in-progress: false +# `previous-app-id` is the App Store id this app shipped under before the +# rename. The store keys everything on the id, so `keepiq` is a brand new +# entry starting from nothing -- without this the version line restarts +# BELOW what the app already published as `doriath`, and the store accepts +# that with a 200 and then never offers it to anyone. Drop the input once +# the `doriath` entry is retired. jobs: unstable: if: github.ref == 'refs/heads/development' @@ -34,6 +40,7 @@ jobs: with: release-type: unstable app-name: keepiq + previous-app-id: doriath secrets: inherit beta: @@ -42,6 +49,7 @@ jobs: with: release-type: beta app-name: keepiq + previous-app-id: doriath secrets: inherit stable: @@ -50,4 +58,5 @@ jobs: with: release-type: stable app-name: keepiq + previous-app-id: doriath secrets: inherit diff --git a/appinfo/info.xml b/appinfo/info.xml index 62d07f7b3..918567c0c 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -36,7 +36,7 @@ Vrij en open source onder de EUPL-1.2-licentie. **Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl. ]]> - 0.1.7-beta.20260820211310 + 0.2.2-unstable.20260830082444 EUPL-1.2 Conduction Keepiq diff --git a/composer.lock b/composer.lock index 726a51d64..f34a814fe 100644 --- a/composer.lock +++ b/composer.lock @@ -1206,16 +1206,16 @@ }, { "name": "conduction/hydra-gates", - "version": "v1.9.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/ConductionNL/.github.git", - "reference": "9801ffdbff17d05f0934742190742d733ac912b4" + "reference": "d143bc27aecbb44a66c843dba89d374628bf9eef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ConductionNL/.github/zipball/9801ffdbff17d05f0934742190742d733ac912b4", - "reference": "9801ffdbff17d05f0934742190742d733ac912b4", + "url": "https://api.github.com/repos/ConductionNL/.github/zipball/d143bc27aecbb44a66c843dba89d374628bf9eef", + "reference": "d143bc27aecbb44a66c843dba89d374628bf9eef", "shasum": "" }, "require": { @@ -1254,9 +1254,9 @@ "support": { "docs": "https://github.com/ConductionNL/.github/blob/main/hydra-gates/README.md", "issues": "https://github.com/ConductionNL/.github/issues", - "source": "https://github.com/ConductionNL/.github/tree/v1.9.0" + "source": "https://github.com/ConductionNL/.github/tree/v1.10.0" }, - "time": "2026-08-22T00:30:18+00:00" + "time": "2026-08-27T16:18:04+00:00" }, { "name": "consolidation/annotated-command", diff --git a/docs/Installation/demo-environment.md b/docs/Installation/demo-environment.md new file mode 100644 index 000000000..01b807eaf --- /dev/null +++ b/docs/Installation/demo-environment.md @@ -0,0 +1,131 @@ +# Run a local demo + +This page gets a working Keepiq running on your own machine in two commands. You end with archiving and retention over registry objects. + +It is a **demo**, not a development environment. Nothing is mounted from a checkout, and that is deliberate — see [What this is not](#what-this-is-not). + +## What you need + +Docker, with Compose v2.23 or newer. Nothing else — no PHP, no Node, no Nextcloud. + +```bash +docker --version +docker compose version +``` + +If `docker compose version` prints v2.22 or older, upgrade first. The compose file declares its scripts inline via `configs`, and older versions ignore the `content:` field **silently** — which produces an instance with no apps installed and nothing in the logs to explain why. + +## Step 1 — get the compose file + +```bash +curl -fsSLO https://raw.githubusercontent.com/ConductionNL/keepiq/development/keepiq-compose.yaml +``` + +A single self-contained file. There is nothing else to fetch and nothing to edit. + +## Step 2 — start it + +```bash +docker compose -f keepiq-compose.yaml up -d +``` + +The first run takes a few minutes: it pulls three images and downloads the application archives. Watch it work if you like: + +```bash +docker compose -f keepiq-compose.yaml logs -f app-installer +``` + +You are looking for: + +``` +==> installing openregister +==> installing thematiq +==> installing integriq +==> installing keepiq +==> apps present: integriq keepiq openregister thematiq +``` + +Then Nextcloud installs itself and enables the apps **in dependency order**. OpenRegister goes first: it owns the registers and schemas the others declare against, and a leaf app enabled before it finds no register to attach to. + +That is done when this returns `"installed":true`: + +```bash +curl -s http://localhost:8616/status.php +``` + +## Step 3 — open the demo + +| What | Where | +| --- | --- | +| **Keepiq** | [http://localhost:8616/apps/keepiq/](http://localhost:8616/apps/keepiq/) | +| Admin interface | [http://localhost:8616](http://localhost:8616) — `admin` / `admin` | + +## What gets installed, and why more than one app + +| App | Why | +| --- | --- | +| `openregister` | **Required.** Every Connext app declares its registers and schemas against OpenRegister. | +| `thematiq` | Optional. Government theming. Absent, the UI renders unthemed rather than wrong. | +| `integriq` | Optional. The connector, for feeding in data from systems you do not control. | +| `keepiq` | The app this page is about. | + +That OpenRegister dependency is **not declared** in `appinfo/info.xml` — no app in the fleet declares an `` dependency — so nothing stops the App Store from installing keepiq without it. It would then load, find no register to attach to, and show you an empty app rather than an error. The compose file encodes the dependency the manifest does not. + +## Verifying it actually worked + +A page loading is not the same as a page working. Nextcloud serves its shell before the app decides whether it has anything to render, so an app URL returns HTTP 200 even when it resolves to nothing at all. A smoke test that checks for a 200 would call that a success. + +Check content instead: + +```bash +# The app answers. Note the credentials: an app page requires a login, so the +# SAME request without -u returns 401, which is not a broken demo — measured +# on a booted demo while writing this page. +curl -s -o /dev/null -w '%{http_code}\n' -u admin:admin -L "http://localhost:8616/apps/keepiq/" + +# OpenRegister has registers — an empty list means the configuration +# was never imported, which is not the same as "nothing configured yet" +curl -s -u admin:admin "http://localhost:8616/apps/openregister/api/registers" | head -c 300 +``` + +## Changing the defaults + +The port and every version are overridable: + +```bash +DEMO_PORT=9000 \ +KEEPIQ_VERSION=1.2.3 \ +docker compose -f keepiq-compose.yaml up -d +``` + +Leaving a version empty resolves the newest release for that app, pre-releases included — which is what most Connext apps still ship, so that is the default. + +## Tearing it down + +```bash +# Stop, keep the data +docker compose -f keepiq-compose.yaml down + +# Stop and delete everything, including the database +docker compose -f keepiq-compose.yaml down -v +``` + +## What this is not + +**It is not a development environment, and it cannot be turned into one by adding a bind mount.** + +Nextcloud installs and updates an app by deleting the app directory and extracting a fresh archive over it. Point that at a checkout and an app-store update will delete your working tree — measured on a development machine on 27 August 2026, where `\OC\Updater::upgradeAppStoreApp` fired on a container restart and removed every top-level file from a bind-mounted checkout, including its `.git` directory. Only the subdirectories it lacked permission to unlink survived. + +So this compose keeps its apps in a named volume and installs them from release archives. That also happens to be the only thing that works: a release archive is a **complete** app carrying `vendor/` and the built `js/` bundle, while a `git clone` carries neither — and a Nextcloud app with no `vendor/` does not fail loudly. It warns once and keeps loading, so the app appears installed while every service that needs a dependency is quietly absent. + +To work *on* these apps rather than *with* them, use the development environment instead. + +## Troubleshooting + +**`app-installer` exits non-zero.** It could not download an archive. Check the log for the URL it tried; the most common cause is a pinned version with no matching release. + +**It stops with `openregister missing; aborting`.** Deliberate. Every other app declares registers against OpenRegister, so a stack without it would start and then fail in a dozen confusing ways instead of one clear one. + +**The UI renders unthemed.** Thematiq is not installed or not enabled. Expected, and cosmetic — the theme resolver renders unthemed rather than wrong when it is absent. + +**Everything returns 404 or a maintenance page after a restart.** Nextcloud is waiting for an upgrade. Run `docker compose -f keepiq-compose.yaml exec -u www-data nextcloud php occ upgrade`. diff --git a/keepiq-compose.yaml b/keepiq-compose.yaml new file mode 100644 index 000000000..ee4bab808 --- /dev/null +++ b/keepiq-compose.yaml @@ -0,0 +1,252 @@ +# Keepiq — local demo environment +# +# docker compose -f keepiq-compose.yaml up -d +# +# Then open http://localhost:8616/apps/keepiq/ +# Admin UI: http://localhost:8616 (admin / admin) +# +# Tear down, including all data: +# docker compose -f keepiq-compose.yaml down -v +# +# --------------------------------------------------------------------------- +# THIS IS A DEMO ENVIRONMENT, NOT A DEVELOPMENT ENVIRONMENT. +# +# Nothing here is bind-mounted from your working copy, and that is deliberate +# rather than merely simpler. Nextcloud installs and updates an app by DELETING +# its directory and extracting a fresh archive over it. Measured 2026-08-27 on +# a development machine: `\OC\Updater::upgradeAppStoreApp` fired on a container +# restart and removed every top-level file from a bind-mounted checkout — +# including its `.git` directory — leaving only the subdirectories it lacked +# permission to unlink. A demo rig has no business pointing at a checkout, so +# this one owns its apps in a named volume. +# +# If you want to work ON these apps rather than WITH them, use the development +# environment instead. This file cannot serve that purpose and should not try. +# +# --------------------------------------------------------------------------- +# WHY RELEASE TARBALLS RATHER THAN `git clone` +# +# A release tarball is a COMPLETE app: it carries `vendor/` and the built `js/` +# bundle. A git checkout carries neither, and a Nextcloud app whose `vendor/` +# is missing does not fail loudly — `include_once` warns and the app keeps +# loading, so the app appears installed while every service that needs a +# dependency is absent. +# +# --------------------------------------------------------------------------- +# WHAT IS INSTALLED, AND WHY MORE THAN ONE APP +# +# openregister REQUIRED. Every Connext app declares its registers and +# schemas against OpenRegister. Note that this dependency is +# NOT declared in appinfo/info.xml — no app in the fleet +# declares an dependency — so nothing stops the App +# Store installing keepiq without it. It would then load +# and find no register to attach to. +# thematiq Optional. Government theming. Absent, the UI renders +# unthemed rather than wrong. +# integriq Optional. The connector, for feeding data in from systems +# you do not control. +# keepiq the app this file is for. +# +# Versions float to the newest release by default, pre-releases included, +# because most Connext apps do not yet publish a stable one. Pin any of them: +# +# KEEPIQ_VERSION=1.2.3 docker compose -f keepiq-compose.yaml up -d +# +# The Nextcloud image is pinned to a MAJOR tag rather than `:latest`. A demo +# that is stopped and restarted weeks later would otherwise boot a drifted +# Nextcloud over its existing data volume, which lands the instance in +# maintenance mode with its apps disabled. + +name: keepiq-demo + +volumes: + db: + nextcloud: + apps: + +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: nextcloud + POSTGRES_DB: nextcloud + volumes: + - db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"] + interval: 5s + timeout: 3s + retries: 20 + + # Downloads each app's release tarball into the shared `apps` volume before + # Nextcloud starts. It runs to completion and exits; Nextcloud waits for that + # exit via `condition: service_completed_successfully`, so there is no window + # in which Nextcloud boots against a half-populated app directory. + # + # Idempotent: an app whose appinfo/info.xml is already present is skipped, so + # `up` on an existing demo does not re-download 100MB per app. + app-installer: + image: alpine:3.20 + restart: "no" + environment: + OPENREGISTER_VERSION: ${OPENREGISTER_VERSION:-} + THEMATIQ_VERSION: ${THEMATIQ_VERSION:-} + INTEGRIQ_VERSION: ${INTEGRIQ_VERSION:-} + KEEPIQ_VERSION: ${KEEPIQ_VERSION:-} + volumes: + - apps:/apps + configs: + - source: install-apps + target: /install-apps.sh + mode: "0755" + command: ["/bin/sh", "/install-apps.sh"] + + nextcloud: + image: nextcloud:34-apache + restart: unless-stopped + ports: + - "${DEMO_PORT:-8616}:80" + depends_on: + db: + condition: service_healthy + app-installer: + condition: service_completed_successfully + environment: + POSTGRES_HOST: db + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: nextcloud + POSTGRES_DB: nextcloud + NEXTCLOUD_ADMIN_USER: admin + NEXTCLOUD_ADMIN_PASSWORD: admin + # The port has to appear here as well as in `ports:`. Nextcloud rejects a + # request whose Host header names a domain it does not trust, and + # "localhost" and "localhost:8616" are different entries. + NEXTCLOUD_TRUSTED_DOMAINS: "localhost localhost:${DEMO_PORT:-8616} 127.0.0.1 127.0.0.1:${DEMO_PORT:-8616}" + # OVERWRITECLIURL IS LOAD-BEARING, NOT COSMETIC. Apps that federate + # advertise this address to peers. It is a local address here, and a + # local address is refused rather than broadcast — which is what keeps a + # demo on somebody's laptop out of the national directory. + OVERWRITECLIURL: "http://localhost:${DEMO_PORT:-8616}" + OVERWRITEPROTOCOL: http + volumes: + - nextcloud:/var/www/html + - apps:/var/www/html/custom_apps + configs: + - source: enable-apps + target: /docker-entrypoint-hooks.d/post-installation/10-enable-connext-apps.sh + mode: "0755" + +configs: + # EVERY SHELL VARIABLE BELOW IS WRITTEN `$$name`, NOT `$name`. + # + # Compose interpolates `$name` inside `configs.content` before the file is + # written, so an un-escaped shell variable arrives as an EMPTY STRING and the + # script runs on silently. Measured while building this file: `$version` was + # blanked, which made the "no version pinned" branch look true for an app + # whose version WAS pinned, and the resulting error named no repository — + # `could not resolve a release for ` — because `$repo` had been blanked too. + # + # `$$` is the escape that survives interpolation and reaches /bin/sh as `$`. + # `${DEMO_PORT:-8616}` is deliberately NOT escaped: that one is Compose's + # to substitute. + install-apps: + content: | + #!/bin/sh + set -eu + apk add --no-cache curl tar jq >/dev/null + + # RESOLVING "NEWEST" TAKES TWO CORRECTIONS, NOT ONE. + # + # 1. The GitHub "latest release" endpoint EXCLUDES prereleases and answers + # 404 for a repository that has only ever shipped them — which reads + # exactly like "no such app". Ask the releases LIST instead. + # + # 2. THE LIST IS NOT ORDERED BY CREATION DATE. Taking the first entry looks + # correct and is not. Measured 2026-08-27 on openregister: + # + # v1.1.6 created 10:17:45 <- returned first + # v1.1.6-unstable.20260827110807 created 11:09:37 <- actually newest + # + # Taking the first entry installs an OLDER build than intended, and the + # failure surfaces far from the cause — as a missing CLASS in a + # different app, not as a version complaint. + # + # Sorting by created_at explicitly is the fix; jq is here for that. + resolve_latest() { + curl -fsSL "https://api.github.com/repos/ConductionNL/$$1/releases?per_page=50" \ + | jq -r '[.[] | select(.draft | not)] | sort_by(.created_at) | last | .tag_name // empty' \ + | sed 's/^v//' + } + + install_app() { + repo="$$1"; appid="$$2"; version="$$3"; asset="$$4" + + if [ -f "/apps/$$appid/appinfo/info.xml" ]; then + echo "==> $$appid already present, skipping" + return 0 + fi + + if [ -z "$$version" ]; then + version="$$(resolve_latest "$$repo")" + [ -n "$$version" ] || { echo "!! could not resolve a release for $$repo"; return 1; } + echo "==> $$appid: no version pinned, resolved $$version" + fi + + url="https://github.com/ConductionNL/$$repo/releases/download/v$$version/$$asset-$$version.tar.gz" + echo "==> installing $$appid $$version" + + # Extract into a staging directory rather than straight into /apps. + # The archive's own top-level directory name is not guaranteed to equal + # the Nextcloud app id — Nextcloud resolves an app by its DIRECTORY + # name, so an archive that unpacks under the old name after a rename + # yields an app that is silently never loaded. Several Connext apps were + # renamed in August 2026, so this is a live concern, not a hypothetical. + rm -rf /tmp/stage && mkdir -p /tmp/stage + curl -fsSL "$$url" | tar -xz -C /tmp/stage + + top="$$(ls /tmp/stage | head -n1)" + if [ "$$top" != "$$appid" ]; then + echo " archive unpacked as '$$top', installing it as '$$appid'" + fi + mv "/tmp/stage/$$top" "/apps/$$appid" + rm -rf /tmp/stage + } + + # OpenRegister is not optional. If it could not be fetched, stop here + # rather than let Nextcloud boot into a dozen confusing downstream + # failures instead of one clear one. + install_app openregister openregister "$$OPENREGISTER_VERSION" openregister + install_app thematiq thematiq "$$THEMATIQ_VERSION" thematiq + install_app integriq integriq "$$INTEGRIQ_VERSION" integriq + install_app keepiq keepiq "$$KEEPIQ_VERSION" keepiq + + [ -f /apps/openregister/appinfo/info.xml ] || { echo "!! openregister missing; aborting"; exit 1; } + + # 33 is www-data inside the Nextcloud image. + chown -R 33:33 /apps + echo "==> apps present: $$(ls /apps | tr '\n' ' ')" + + enable-apps: + content: | + #!/bin/sh + set -eu + # Runs once, after Nextcloud has installed itself. + # + # ORDER IS NOT ARBITRARY. OpenRegister owns the registers and schemas the + # other apps declare against, and a leaf app enabled before it finds no + # register to attach to. Enabling them in dependency order is what makes + # a first boot produce a working instance instead of an empty one. + for app in openregister thematiq integriq keepiq; do + echo "==> enabling $$app" + php /var/www/html/occ app:enable "$$app" || echo "!! failed to enable $$app" + done + + # Several apps ship a schema whose slug is not unique across the + # instance. OpenRegister resolves a duplicate slug by tie-break and warns, + # which means a leaf app can silently read another app's schema. This is a + # no-op on a clean install and a repair on one that has drifted. + php /var/www/html/occ openregister:schemas:dedup || true + + echo "==> Keepiq demo: http://localhost:${DEMO_PORT:-8616}/apps/keepiq/" diff --git a/l10n/be.js b/l10n/be.js index 393c5d0c7..7c027c490 100644 --- a/l10n/be.js +++ b/l10n/be.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Выдаліць гэта поле", "Give the field a name.": "Дайце полю назву.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» — убудаванае поле, а не дадатковае: выберыце іншую назву.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Элемент", + "No recent activity": "Няма нядаўняй актыўнасці", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Абнавіць", + "No secrets found": "Сакрэтаў не знойдзена", + "Add your first secret using the button above": "Дадайце свой першы сакрэт кнопкай вышэй", + "More actions": "Больш дзеянняў", + "New vault": "Новае сховішча", + "Filter by type": "Фільтраваць па тыпе", + "Vaults": "Сховішчы", + "All vaults": "Усе сховішчы", + "Admin settings": "Налады адміністратара", + "Opens in a new tab": "Адкрываецца ў новай укладцы", + "Keepiq navigation": "Навігацыя Keepiq", + "Vault name": "Назва сховішча", + "Create vault": "Стварыць сховішча" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/be.json b/l10n/be.json index bce470960..d05a68f42 100644 --- a/l10n/be.json +++ b/l10n/be.json @@ -426,7 +426,28 @@ "Remove this field": "Выдаліць гэта поле", "Give the field a name.": "Дайце полю назву.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» — убудаванае поле, а не дадатковае: выберыце іншую назву.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Элемент", + "No recent activity": "Няма нядаўняй актыўнасці", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Абнавіць", + "No secrets found": "Сакрэтаў не знойдзена", + "Add your first secret using the button above": "Дадайце свой першы сакрэт кнопкай вышэй", + "More actions": "Больш дзеянняў", + "New vault": "Новае сховішча", + "Filter by type": "Фільтраваць па тыпе", + "Vaults": "Сховішчы", + "All vaults": "Усе сховішчы", + "Admin settings": "Налады адміністратара", + "Opens in a new tab": "Адкрываецца ў новай укладцы", + "Keepiq navigation": "Навігацыя Keepiq", + "Vault name": "Назва сховішча", + "Create vault": "Стварыць сховішча" }, "plurals": null } diff --git a/l10n/bg.js b/l10n/bg.js index 4aee466cd..5283e0f0e 100644 --- a/l10n/bg.js +++ b/l10n/bg.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Премахване на това поле", "Give the field a name.": "Дайте име на полето.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ е вградено поле, а не допълнително — изберете друго име.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Елемент", + "No recent activity": "Няма скорошна активност", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Опресняване", + "No secrets found": "Няма намерени тайни", + "Add your first secret using the button above": "Добавете първата си тайна чрез бутона по-горе", + "More actions": "Още действия", + "New vault": "Нов трезор", + "Filter by type": "Филтриране по тип", + "Vaults": "Трезори", + "All vaults": "Всички трезори", + "Admin settings": "Административни настройки", + "Opens in a new tab": "Отваря се в нов раздел", + "Keepiq navigation": "Навигация на Keepiq", + "Vault name": "Име на трезора", + "Create vault": "Създаване на трезор" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/bg.json b/l10n/bg.json index 201c9238b..918df8b38 100644 --- a/l10n/bg.json +++ b/l10n/bg.json @@ -426,7 +426,28 @@ "Remove this field": "Премахване на това поле", "Give the field a name.": "Дайте име на полето.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ е вградено поле, а не допълнително — изберете друго име.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Елемент", + "No recent activity": "Няма скорошна активност", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Опресняване", + "No secrets found": "Няма намерени тайни", + "Add your first secret using the button above": "Добавете първата си тайна чрез бутона по-горе", + "More actions": "Още действия", + "New vault": "Нов трезор", + "Filter by type": "Филтриране по тип", + "Vaults": "Трезори", + "All vaults": "Всички трезори", + "Admin settings": "Административни настройки", + "Opens in a new tab": "Отваря се в нов раздел", + "Keepiq navigation": "Навигация на Keepiq", + "Vault name": "Име на трезора", + "Create vault": "Създаване на трезор" }, "plurals": null } diff --git a/l10n/bs.js b/l10n/bs.js index 082087acb..a4ad878dc 100644 --- a/l10n/bs.js +++ b/l10n/bs.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Ukloni ovo polje", "Give the field a name.": "Dajte polju naziv.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je ugrađeno polje, a ne dodatno — odaberite drugi naziv.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Stavka", + "No recent activity": "Nema nedavnih aktivnosti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osvježi", + "No secrets found": "Nije pronađena nijedna tajna", + "Add your first secret using the button above": "Dodajte svoju prvu tajnu dugmetom iznad", + "More actions": "Više radnji", + "New vault": "Novi trezor", + "Filter by type": "Filtriraj po tipu", + "Vaults": "Trezori", + "All vaults": "Svi trezori", + "Admin settings": "Administratorske postavke", + "Opens in a new tab": "Otvara se u novoj kartici", + "Keepiq navigation": "Keepiq navigacija", + "Vault name": "Naziv trezora", + "Create vault": "Kreiraj trezor" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/bs.json b/l10n/bs.json index a86933009..4d8a7ca53 100644 --- a/l10n/bs.json +++ b/l10n/bs.json @@ -426,7 +426,28 @@ "Remove this field": "Ukloni ovo polje", "Give the field a name.": "Dajte polju naziv.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je ugrađeno polje, a ne dodatno — odaberite drugi naziv.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Stavka", + "No recent activity": "Nema nedavnih aktivnosti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osvježi", + "No secrets found": "Nije pronađena nijedna tajna", + "Add your first secret using the button above": "Dodajte svoju prvu tajnu dugmetom iznad", + "More actions": "Više radnji", + "New vault": "Novi trezor", + "Filter by type": "Filtriraj po tipu", + "Vaults": "Trezori", + "All vaults": "Svi trezori", + "Admin settings": "Administratorske postavke", + "Opens in a new tab": "Otvara se u novoj kartici", + "Keepiq navigation": "Keepiq navigacija", + "Vault name": "Naziv trezora", + "Create vault": "Kreiraj trezor" }, "plurals": null } diff --git a/l10n/ca.js b/l10n/ca.js index 7bc790de7..e716e3ba5 100644 --- a/l10n/ca.js +++ b/l10n/ca.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Elimina aquest camp", "Give the field a name.": "Doneu un nom al camp.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» és un camp integrat, no addicional: trieu un altre nom.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Sense activitat recent", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Actualitza", + "No secrets found": "No s’han trobat secrets", + "Add your first secret using the button above": "Afegiu el vostre primer secret amb el botó de dalt", + "More actions": "Més accions", + "New vault": "Nova caixa forta", + "Filter by type": "Filtra per tipus", + "Vaults": "Caixes fortes", + "All vaults": "Totes les caixes fortes", + "Admin settings": "Paràmetres d’administració", + "Opens in a new tab": "S’obre en una pestanya nova", + "Keepiq navigation": "Navegació de Keepiq", + "Vault name": "Nom de la caixa forta", + "Create vault": "Crea la caixa forta" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ca.json b/l10n/ca.json index 0bcd1d31e..859197b15 100644 --- a/l10n/ca.json +++ b/l10n/ca.json @@ -426,7 +426,28 @@ "Remove this field": "Elimina aquest camp", "Give the field a name.": "Doneu un nom al camp.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» és un camp integrat, no addicional: trieu un altre nom.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Sense activitat recent", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Actualitza", + "No secrets found": "No s’han trobat secrets", + "Add your first secret using the button above": "Afegiu el vostre primer secret amb el botó de dalt", + "More actions": "Més accions", + "New vault": "Nova caixa forta", + "Filter by type": "Filtra per tipus", + "Vaults": "Caixes fortes", + "All vaults": "Totes les caixes fortes", + "Admin settings": "Paràmetres d’administració", + "Opens in a new tab": "S’obre en una pestanya nova", + "Keepiq navigation": "Navegació de Keepiq", + "Vault name": "Nom de la caixa forta", + "Create vault": "Crea la caixa forta" }, "plurals": null } diff --git a/l10n/cs.js b/l10n/cs.js index 6b9e93528..90581d40b 100644 --- a/l10n/cs.js +++ b/l10n/cs.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Odebrat toto pole", "Give the field a name.": "Pojmenujte pole.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je vestavěné pole, nikoli další — zvolte jiný název.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Položka", + "No recent activity": "Žádná nedávná aktivita", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Obnovit", + "No secrets found": "Nebyla nalezena žádná tajemství", + "Add your first secret using the button above": "Přidejte své první tajemství tlačítkem výše", + "More actions": "Další akce", + "New vault": "Nový trezor", + "Filter by type": "Filtrovat podle typu", + "Vaults": "Trezory", + "All vaults": "Všechny trezory", + "Admin settings": "Nastavení správy", + "Opens in a new tab": "Otevře se na nové kartě", + "Keepiq navigation": "Navigace Keepiq", + "Vault name": "Název trezoru", + "Create vault": "Vytvořit trezor" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/cs.json b/l10n/cs.json index 8df63263e..a6aeb0fd2 100644 --- a/l10n/cs.json +++ b/l10n/cs.json @@ -426,7 +426,28 @@ "Remove this field": "Odebrat toto pole", "Give the field a name.": "Pojmenujte pole.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je vestavěné pole, nikoli další — zvolte jiný název.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Položka", + "No recent activity": "Žádná nedávná aktivita", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Obnovit", + "No secrets found": "Nebyla nalezena žádná tajemství", + "Add your first secret using the button above": "Přidejte své první tajemství tlačítkem výše", + "More actions": "Další akce", + "New vault": "Nový trezor", + "Filter by type": "Filtrovat podle typu", + "Vaults": "Trezory", + "All vaults": "Všechny trezory", + "Admin settings": "Nastavení správy", + "Opens in a new tab": "Otevře se na nové kartě", + "Keepiq navigation": "Navigace Keepiq", + "Vault name": "Název trezoru", + "Create vault": "Vytvořit trezor" }, "plurals": null } diff --git a/l10n/da.js b/l10n/da.js index e20915af2..65b8d5ae5 100644 --- a/l10n/da.js +++ b/l10n/da.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Fjern dette felt", "Give the field a name.": "Giv feltet et navn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” er et indbygget felt, ikke et yderligere — vælg et andet navn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Ingen nylig aktivitet", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Opdater", + "No secrets found": "Ingen hemmeligheder fundet", + "Add your first secret using the button above": "Tilføj din første hemmelighed med knappen ovenfor", + "More actions": "Flere handlinger", + "New vault": "Ny boks", + "Filter by type": "Filtrér efter type", + "Vaults": "Bokse", + "All vaults": "Alle bokse", + "Admin settings": "Administrationsindstillinger", + "Opens in a new tab": "Åbnes i en ny fane", + "Keepiq navigation": "Keepiq-navigation", + "Vault name": "Boksnavn", + "Create vault": "Opret boks" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/da.json b/l10n/da.json index af72f96c1..6d73392ce 100644 --- a/l10n/da.json +++ b/l10n/da.json @@ -426,7 +426,28 @@ "Remove this field": "Fjern dette felt", "Give the field a name.": "Giv feltet et navn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” er et indbygget felt, ikke et yderligere — vælg et andet navn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Ingen nylig aktivitet", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Opdater", + "No secrets found": "Ingen hemmeligheder fundet", + "Add your first secret using the button above": "Tilføj din første hemmelighed med knappen ovenfor", + "More actions": "Flere handlinger", + "New vault": "Ny boks", + "Filter by type": "Filtrér efter type", + "Vaults": "Bokse", + "All vaults": "Alle bokse", + "Admin settings": "Administrationsindstillinger", + "Opens in a new tab": "Åbnes i en ny fane", + "Keepiq navigation": "Keepiq-navigation", + "Vault name": "Boksnavn", + "Create vault": "Opret boks" }, "plurals": null } diff --git a/l10n/de.js b/l10n/de.js index c36e68a3f..51fcc7c69 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Dieses Feld entfernen", "Give the field a name.": "Geben Sie dem Feld einen Namen.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ ist ein integriertes Feld, kein zusätzliches — wählen Sie einen anderen Namen.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Keine aktuellen Aktivitäten", + "View all": "Alle anzeigen", + "Applications awaiting approval": "Anwendungen, die auf Genehmigung warten", + "No applications awaiting approval": "Keine Anwendungen warten auf Genehmigung", + "Vault overview": "Tresorübersicht", + "Your secrets, folders and applications at a glance": "Ihre Geheimnisse, Ordner und Anwendungen auf einen Blick", + "Rotation due": "Rotation fällig", + "Refresh": "Aktualisieren", + "No secrets found": "Keine Geheimnisse gefunden", + "Add your first secret using the button above": "Fügen Sie Ihr erstes Geheimnis über die Schaltfläche oben hinzu", + "More actions": "Weitere Aktionen", + "New vault": "Neuer Tresor", + "Filter by type": "Nach Typ filtern", + "Vaults": "Tresore", + "All vaults": "Alle Tresore", + "Admin settings": "Verwaltungseinstellungen", + "Opens in a new tab": "Wird in einem neuen Tab geöffnet", + "Keepiq navigation": "Keepiq-Navigation", + "Vault name": "Tresorname", + "Create vault": "Tresor erstellen" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/de.json b/l10n/de.json index 7560be913..d3eeb3abb 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -426,7 +426,28 @@ "Remove this field": "Dieses Feld entfernen", "Give the field a name.": "Geben Sie dem Feld einen Namen.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ ist ein integriertes Feld, kein zusätzliches — wählen Sie einen anderen Namen.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Keine aktuellen Aktivitäten", + "View all": "Alle anzeigen", + "Applications awaiting approval": "Anwendungen, die auf Genehmigung warten", + "No applications awaiting approval": "Keine Anwendungen warten auf Genehmigung", + "Vault overview": "Tresorübersicht", + "Your secrets, folders and applications at a glance": "Ihre Geheimnisse, Ordner und Anwendungen auf einen Blick", + "Rotation due": "Rotation fällig", + "Refresh": "Aktualisieren", + "No secrets found": "Keine Geheimnisse gefunden", + "Add your first secret using the button above": "Fügen Sie Ihr erstes Geheimnis über die Schaltfläche oben hinzu", + "More actions": "Weitere Aktionen", + "New vault": "Neuer Tresor", + "Filter by type": "Nach Typ filtern", + "Vaults": "Tresore", + "All vaults": "Alle Tresore", + "Admin settings": "Verwaltungseinstellungen", + "Opens in a new tab": "Wird in einem neuen Tab geöffnet", + "Keepiq navigation": "Keepiq-Navigation", + "Vault name": "Tresorname", + "Create vault": "Tresor erstellen" }, "plurals": null } diff --git a/l10n/el.js b/l10n/el.js index 7acb2be18..dc7f657db 100644 --- a/l10n/el.js +++ b/l10n/el.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Αφαίρεση αυτού του πεδίου", "Give the field a name.": "Δώστε όνομα στο πεδίο.", "“{name}” is a built-in field, not an additional one — choose a different name.": "Το «{name}» είναι ενσωματωμένο πεδίο, όχι πρόσθετο — επιλέξτε άλλο όνομα.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Στοιχείο", + "No recent activity": "Καμία πρόσφατη δραστηριότητα", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Ανανέωση", + "No secrets found": "Δεν βρέθηκαν μυστικά", + "Add your first secret using the button above": "Προσθέστε το πρώτο σας μυστικό με το παραπάνω κουμπί", + "More actions": "Περισσότερες ενέργειες", + "New vault": "Νέο θησαυροφυλάκιο", + "Filter by type": "Φιλτράρισμα ανά τύπο", + "Vaults": "Θησαυροφυλάκια", + "All vaults": "Όλα τα θησαυροφυλάκια", + "Admin settings": "Ρυθμίσεις διαχείρισης", + "Opens in a new tab": "Ανοίγει σε νέα καρτέλα", + "Keepiq navigation": "Πλοήγηση Keepiq", + "Vault name": "Όνομα θησαυροφυλακίου", + "Create vault": "Δημιουργία θησαυροφυλακίου" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/el.json b/l10n/el.json index 89509d193..f75df6e87 100644 --- a/l10n/el.json +++ b/l10n/el.json @@ -426,7 +426,28 @@ "Remove this field": "Αφαίρεση αυτού του πεδίου", "Give the field a name.": "Δώστε όνομα στο πεδίο.", "“{name}” is a built-in field, not an additional one — choose a different name.": "Το «{name}» είναι ενσωματωμένο πεδίο, όχι πρόσθετο — επιλέξτε άλλο όνομα.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Στοιχείο", + "No recent activity": "Καμία πρόσφατη δραστηριότητα", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Ανανέωση", + "No secrets found": "Δεν βρέθηκαν μυστικά", + "Add your first secret using the button above": "Προσθέστε το πρώτο σας μυστικό με το παραπάνω κουμπί", + "More actions": "Περισσότερες ενέργειες", + "New vault": "Νέο θησαυροφυλάκιο", + "Filter by type": "Φιλτράρισμα ανά τύπο", + "Vaults": "Θησαυροφυλάκια", + "All vaults": "Όλα τα θησαυροφυλάκια", + "Admin settings": "Ρυθμίσεις διαχείρισης", + "Opens in a new tab": "Ανοίγει σε νέα καρτέλα", + "Keepiq navigation": "Πλοήγηση Keepiq", + "Vault name": "Όνομα θησαυροφυλακίου", + "Create vault": "Δημιουργία θησαυροφυλακίου" }, "plurals": null } diff --git a/l10n/en.js b/l10n/en.js index 72e74b352..fd37ff1da 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -996,7 +996,27 @@ OC.L10N.register( "Remove": "Remove", "Remove this field": "Remove this field", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” is a built-in field, not an additional one — choose a different name.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Item", + "No recent activity": "No recent activity", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Add your first secret using the button above": "Add your first secret using the button above", + "Filter by type": "Filter by type", + "More actions": "More actions", + "New vault": "New vault", + "No secrets found": "No secrets found", + "Refresh": "Refresh", + "Admin settings": "Admin settings", + "All vaults": "All vaults", + "Keepiq navigation": "Keepiq navigation", + "Opens in a new tab": "Opens in a new tab", + "Vaults": "Vaults", + "Create vault": "Create vault", + "Vault name": "Vault name" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index 5852d24da..92c5ac554 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -995,7 +995,27 @@ "Remove": "Remove", "Remove this field": "Remove this field", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” is a built-in field, not an additional one — choose a different name.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Item", + "No recent activity": "No recent activity", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Add your first secret using the button above": "Add your first secret using the button above", + "Filter by type": "Filter by type", + "More actions": "More actions", + "New vault": "New vault", + "No secrets found": "No secrets found", + "Refresh": "Refresh", + "Admin settings": "Admin settings", + "All vaults": "All vaults", + "Keepiq navigation": "Keepiq navigation", + "Opens in a new tab": "Opens in a new tab", + "Vaults": "Vaults", + "Create vault": "Create vault", + "Vault name": "Vault name" }, "plurals": "", "pluralForm": "nplurals=2; plural=(n != 1);" diff --git a/l10n/es.js b/l10n/es.js index 004f7f4ad..0bbd30eef 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Eliminar este campo", "Give the field a name.": "Dale un nombre al campo.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» es un campo integrado, no adicional: elige otro nombre.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elemento", + "No recent activity": "Sin actividad reciente", + "View all": "Ver todo", + "Applications awaiting approval": "Aplicaciones pendientes de aprobación", + "No applications awaiting approval": "No hay aplicaciones pendientes de aprobación", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Actualizar", + "No secrets found": "No se encontraron secretos", + "Add your first secret using the button above": "Añada su primer secreto con el botón de arriba", + "More actions": "Más acciones", + "New vault": "Nueva caja fuerte", + "Filter by type": "Filtrar por tipo", + "Vaults": "Cajas fuertes", + "All vaults": "Todas las cajas fuertes", + "Admin settings": "Ajustes de administración", + "Opens in a new tab": "Se abre en una pestaña nueva", + "Keepiq navigation": "Navegación de Keepiq", + "Vault name": "Nombre de la caja fuerte", + "Create vault": "Crear caja fuerte" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/es.json b/l10n/es.json index 0d241982f..40795f837 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -426,7 +426,28 @@ "Remove this field": "Eliminar este campo", "Give the field a name.": "Dale un nombre al campo.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» es un campo integrado, no adicional: elige otro nombre.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elemento", + "No recent activity": "Sin actividad reciente", + "View all": "Ver todo", + "Applications awaiting approval": "Aplicaciones pendientes de aprobación", + "No applications awaiting approval": "No hay aplicaciones pendientes de aprobación", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Actualizar", + "No secrets found": "No se encontraron secretos", + "Add your first secret using the button above": "Añada su primer secreto con el botón de arriba", + "More actions": "Más acciones", + "New vault": "Nueva caja fuerte", + "Filter by type": "Filtrar por tipo", + "Vaults": "Cajas fuertes", + "All vaults": "Todas las cajas fuertes", + "Admin settings": "Ajustes de administración", + "Opens in a new tab": "Se abre en una pestaña nueva", + "Keepiq navigation": "Navegación de Keepiq", + "Vault name": "Nombre de la caja fuerte", + "Create vault": "Crear caja fuerte" }, "plurals": null } diff --git a/l10n/et.js b/l10n/et.js index a1e36c16f..5065c4d7d 100644 --- a/l10n/et.js +++ b/l10n/et.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Eemalda see väli", "Give the field a name.": "Anna väljale nimi.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ on sisseehitatud väli, mitte lisaväli — vali teine nimi.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Üksus", + "No recent activity": "Hiljutine tegevus puudub", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Värskenda", + "No secrets found": "Ühtegi saladust ei leitud", + "Add your first secret using the button above": "Lisa oma esimene saladus ülaloleva nupuga", + "More actions": "Rohkem toiminguid", + "New vault": "Uus hoidla", + "Filter by type": "Filtreeri tüübi järgi", + "Vaults": "Hoidlad", + "All vaults": "Kõik hoidlad", + "Admin settings": "Halduri seaded", + "Opens in a new tab": "Avaneb uuel vahekaardil", + "Keepiq navigation": "Keepiqi navigeerimine", + "Vault name": "Hoidla nimi", + "Create vault": "Loo hoidla" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/et.json b/l10n/et.json index f5d206c7a..292732370 100644 --- a/l10n/et.json +++ b/l10n/et.json @@ -426,7 +426,28 @@ "Remove this field": "Eemalda see väli", "Give the field a name.": "Anna väljale nimi.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ on sisseehitatud väli, mitte lisaväli — vali teine nimi.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Üksus", + "No recent activity": "Hiljutine tegevus puudub", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Värskenda", + "No secrets found": "Ühtegi saladust ei leitud", + "Add your first secret using the button above": "Lisa oma esimene saladus ülaloleva nupuga", + "More actions": "Rohkem toiminguid", + "New vault": "Uus hoidla", + "Filter by type": "Filtreeri tüübi järgi", + "Vaults": "Hoidlad", + "All vaults": "Kõik hoidlad", + "Admin settings": "Halduri seaded", + "Opens in a new tab": "Avaneb uuel vahekaardil", + "Keepiq navigation": "Keepiqi navigeerimine", + "Vault name": "Hoidla nimi", + "Create vault": "Loo hoidla" }, "plurals": null } diff --git a/l10n/fi.js b/l10n/fi.js index e0d61ae54..1ce2b5b64 100644 --- a/l10n/fi.js +++ b/l10n/fi.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Poista tämä kenttä", "Give the field a name.": "Anna kentälle nimi.", "“{name}” is a built-in field, not an additional one — choose a different name.": "”{name}” on sisäänrakennettu kenttä, ei lisäkenttä — valitse toinen nimi.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Kohde", + "No recent activity": "Ei viimeaikaista toimintaa", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Päivitä", + "No secrets found": "Salaisuuksia ei löytynyt", + "Add your first secret using the button above": "Lisää ensimmäinen salaisuutesi yllä olevalla painikkeella", + "More actions": "Lisää toimintoja", + "New vault": "Uusi holvi", + "Filter by type": "Suodata tyypin mukaan", + "Vaults": "Holvit", + "All vaults": "Kaikki holvit", + "Admin settings": "Hallinta-asetukset", + "Opens in a new tab": "Avautuu uuteen välilehteen", + "Keepiq navigation": "Keepiqin navigointi", + "Vault name": "Holvin nimi", + "Create vault": "Luo holvi" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/fi.json b/l10n/fi.json index 82b694ead..66ac4479b 100644 --- a/l10n/fi.json +++ b/l10n/fi.json @@ -426,7 +426,28 @@ "Remove this field": "Poista tämä kenttä", "Give the field a name.": "Anna kentälle nimi.", "“{name}” is a built-in field, not an additional one — choose a different name.": "”{name}” on sisäänrakennettu kenttä, ei lisäkenttä — valitse toinen nimi.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Kohde", + "No recent activity": "Ei viimeaikaista toimintaa", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Päivitä", + "No secrets found": "Salaisuuksia ei löytynyt", + "Add your first secret using the button above": "Lisää ensimmäinen salaisuutesi yllä olevalla painikkeella", + "More actions": "Lisää toimintoja", + "New vault": "Uusi holvi", + "Filter by type": "Suodata tyypin mukaan", + "Vaults": "Holvit", + "All vaults": "Kaikki holvit", + "Admin settings": "Hallinta-asetukset", + "Opens in a new tab": "Avautuu uuteen välilehteen", + "Keepiq navigation": "Keepiqin navigointi", + "Vault name": "Holvin nimi", + "Create vault": "Luo holvi" }, "plurals": null } diff --git a/l10n/fr.js b/l10n/fr.js index 696160514..e76aab1d1 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Supprimer ce champ", "Give the field a name.": "Donnez un nom au champ.", "“{name}” is a built-in field, not an additional one — choose a different name.": "« {name} » est un champ intégré, pas un champ supplémentaire — choisissez un autre nom.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Élément", + "No recent activity": "Aucune activité récente", + "View all": "Tout afficher", + "Applications awaiting approval": "Applications en attente d’approbation", + "No applications awaiting approval": "Aucune application en attente d’approbation", + "Vault overview": "Aperçu du coffre", + "Your secrets, folders and applications at a glance": "Vos secrets, dossiers et applications en un coup d’œil", + "Rotation due": "Rotation requise", + "Refresh": "Actualiser", + "No secrets found": "Aucun secret trouvé", + "Add your first secret using the button above": "Ajoutez votre premier secret à l’aide du bouton ci-dessus", + "More actions": "Plus d’actions", + "New vault": "Nouveau coffre-fort", + "Filter by type": "Filtrer par type", + "Vaults": "Coffres-forts", + "All vaults": "Tous les coffres-forts", + "Admin settings": "Paramètres d’administration", + "Opens in a new tab": "S’ouvre dans un nouvel onglet", + "Keepiq navigation": "Navigation Keepiq", + "Vault name": "Nom du coffre-fort", + "Create vault": "Créer le coffre-fort" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/fr.json b/l10n/fr.json index 80b0a395a..5c5dcec9d 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -426,7 +426,28 @@ "Remove this field": "Supprimer ce champ", "Give the field a name.": "Donnez un nom au champ.", "“{name}” is a built-in field, not an additional one — choose a different name.": "« {name} » est un champ intégré, pas un champ supplémentaire — choisissez un autre nom.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Élément", + "No recent activity": "Aucune activité récente", + "View all": "Tout afficher", + "Applications awaiting approval": "Applications en attente d’approbation", + "No applications awaiting approval": "Aucune application en attente d’approbation", + "Vault overview": "Aperçu du coffre", + "Your secrets, folders and applications at a glance": "Vos secrets, dossiers et applications en un coup d’œil", + "Rotation due": "Rotation requise", + "Refresh": "Actualiser", + "No secrets found": "Aucun secret trouvé", + "Add your first secret using the button above": "Ajoutez votre premier secret à l’aide du bouton ci-dessus", + "More actions": "Plus d’actions", + "New vault": "Nouveau coffre-fort", + "Filter by type": "Filtrer par type", + "Vaults": "Coffres-forts", + "All vaults": "Tous les coffres-forts", + "Admin settings": "Paramètres d’administration", + "Opens in a new tab": "S’ouvre dans un nouvel onglet", + "Keepiq navigation": "Navigation Keepiq", + "Vault name": "Nom du coffre-fort", + "Create vault": "Créer le coffre-fort" }, "plurals": null } diff --git a/l10n/ga.js b/l10n/ga.js index e61d9e5a6..7f2f801ce 100644 --- a/l10n/ga.js +++ b/l10n/ga.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Bain an réimse seo", "Give the field a name.": "Tabhair ainm ar an réimse.", "“{name}” is a built-in field, not an additional one — choose a different name.": "Is réimse ionsuite é “{name}”, nach ceann breise — roghnaigh ainm eile.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Mír", + "No recent activity": "Níl aon ghníomhaíocht le déanaí", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Athnuaigh", + "No secrets found": "Níor aimsíodh aon rúin", + "Add your first secret using the button above": "Cuir do chéad rún leis an gcnaipe thuas", + "More actions": "Tuilleadh gníomhartha", + "New vault": "Taisceadán nua", + "Filter by type": "Scag de réir cineáil", + "Vaults": "Taisceadáin", + "All vaults": "Gach taisceadán", + "Admin settings": "Socruithe riaracháin", + "Opens in a new tab": "Osclaítear i gcluaisín nua é", + "Keepiq navigation": "Nascleanúint Keepiq", + "Vault name": "Ainm an taisceadáin", + "Create vault": "Cruthaigh taisceadán" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ga.json b/l10n/ga.json index 2cb336496..dd78d7749 100644 --- a/l10n/ga.json +++ b/l10n/ga.json @@ -426,7 +426,28 @@ "Remove this field": "Bain an réimse seo", "Give the field a name.": "Tabhair ainm ar an réimse.", "“{name}” is a built-in field, not an additional one — choose a different name.": "Is réimse ionsuite é “{name}”, nach ceann breise — roghnaigh ainm eile.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Mír", + "No recent activity": "Níl aon ghníomhaíocht le déanaí", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Athnuaigh", + "No secrets found": "Níor aimsíodh aon rúin", + "Add your first secret using the button above": "Cuir do chéad rún leis an gcnaipe thuas", + "More actions": "Tuilleadh gníomhartha", + "New vault": "Taisceadán nua", + "Filter by type": "Scag de réir cineáil", + "Vaults": "Taisceadáin", + "All vaults": "Gach taisceadán", + "Admin settings": "Socruithe riaracháin", + "Opens in a new tab": "Osclaítear i gcluaisín nua é", + "Keepiq navigation": "Nascleanúint Keepiq", + "Vault name": "Ainm an taisceadáin", + "Create vault": "Cruthaigh taisceadán" }, "plurals": null } diff --git a/l10n/hr.js b/l10n/hr.js index f655c202d..52408df59 100644 --- a/l10n/hr.js +++ b/l10n/hr.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Ukloni ovo polje", "Give the field a name.": "Dajte polju naziv.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je ugrađeno polje, a ne dodatno — odaberite drugi naziv.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Stavka", + "No recent activity": "Nema nedavnih aktivnosti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osvježi", + "No secrets found": "Nije pronađena nijedna tajna", + "Add your first secret using the button above": "Dodajte svoju prvu tajnu pomoću gumba iznad", + "More actions": "Više radnji", + "New vault": "Novi trezor", + "Filter by type": "Filtriraj po vrsti", + "Vaults": "Trezori", + "All vaults": "Svi trezori", + "Admin settings": "Administratorske postavke", + "Opens in a new tab": "Otvara se u novoj kartici", + "Keepiq navigation": "Keepiq navigacija", + "Vault name": "Naziv trezora", + "Create vault": "Stvori trezor" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/hr.json b/l10n/hr.json index aa6b173fe..2c23d4695 100644 --- a/l10n/hr.json +++ b/l10n/hr.json @@ -426,7 +426,28 @@ "Remove this field": "Ukloni ovo polje", "Give the field a name.": "Dajte polju naziv.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je ugrađeno polje, a ne dodatno — odaberite drugi naziv.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Stavka", + "No recent activity": "Nema nedavnih aktivnosti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osvježi", + "No secrets found": "Nije pronađena nijedna tajna", + "Add your first secret using the button above": "Dodajte svoju prvu tajnu pomoću gumba iznad", + "More actions": "Više radnji", + "New vault": "Novi trezor", + "Filter by type": "Filtriraj po vrsti", + "Vaults": "Trezori", + "All vaults": "Svi trezori", + "Admin settings": "Administratorske postavke", + "Opens in a new tab": "Otvara se u novoj kartici", + "Keepiq navigation": "Keepiq navigacija", + "Vault name": "Naziv trezora", + "Create vault": "Stvori trezor" }, "plurals": null } diff --git a/l10n/hu.js b/l10n/hu.js index 56d39f712..bed4548e6 100644 --- a/l10n/hu.js +++ b/l10n/hu.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Mező eltávolítása", "Give the field a name.": "Adjon nevet a mezőnek.", "“{name}” is a built-in field, not an additional one — choose a different name.": "A(z) „{name}” beépített mező, nem további — válasszon másik nevet.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elem", + "No recent activity": "Nincs friss tevékenység", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Frissítés", + "No secrets found": "Nem található titok", + "Add your first secret using the button above": "Adja hozzá első titkát a fenti gombbal", + "More actions": "További műveletek", + "New vault": "Új széf", + "Filter by type": "Szűrés típus szerint", + "Vaults": "Széfek", + "All vaults": "Összes széf", + "Admin settings": "Rendszergazdai beállítások", + "Opens in a new tab": "Új lapon nyílik meg", + "Keepiq navigation": "Keepiq navigáció", + "Vault name": "Széf neve", + "Create vault": "Széf létrehozása" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/hu.json b/l10n/hu.json index d2b9f6a92..1be7506fd 100644 --- a/l10n/hu.json +++ b/l10n/hu.json @@ -426,7 +426,28 @@ "Remove this field": "Mező eltávolítása", "Give the field a name.": "Adjon nevet a mezőnek.", "“{name}” is a built-in field, not an additional one — choose a different name.": "A(z) „{name}” beépített mező, nem további — válasszon másik nevet.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elem", + "No recent activity": "Nincs friss tevékenység", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Frissítés", + "No secrets found": "Nem található titok", + "Add your first secret using the button above": "Adja hozzá első titkát a fenti gombbal", + "More actions": "További műveletek", + "New vault": "Új széf", + "Filter by type": "Szűrés típus szerint", + "Vaults": "Széfek", + "All vaults": "Összes széf", + "Admin settings": "Rendszergazdai beállítások", + "Opens in a new tab": "Új lapon nyílik meg", + "Keepiq navigation": "Keepiq navigáció", + "Vault name": "Széf neve", + "Create vault": "Széf létrehozása" }, "plurals": null } diff --git a/l10n/is.js b/l10n/is.js index bc9186c4e..4c359450d 100644 --- a/l10n/is.js +++ b/l10n/is.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Fjarlægja þetta svið", "Give the field a name.": "Gefðu sviðinu nafn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ er innbyggt svið, ekki viðbótarsvið — veldu annað nafn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Atriði", + "No recent activity": "Engin nýleg virkni", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Uppfæra", + "No secrets found": "Engin leyndarmál fundust", + "Add your first secret using the button above": "Bættu við fyrsta leyndarmálinu þínu með hnappnum að ofan", + "More actions": "Fleiri aðgerðir", + "New vault": "Ný hvelfing", + "Filter by type": "Sía eftir tegund", + "Vaults": "Hvelfingar", + "All vaults": "Allar hvelfingar", + "Admin settings": "Stjórnunarstillingar", + "Opens in a new tab": "Opnast í nýjum flipa", + "Keepiq navigation": "Keepiq-leiðsögn", + "Vault name": "Nafn hvelfingar", + "Create vault": "Búa til hvelfingu" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/is.json b/l10n/is.json index 140266696..66469cc3e 100644 --- a/l10n/is.json +++ b/l10n/is.json @@ -426,7 +426,28 @@ "Remove this field": "Fjarlægja þetta svið", "Give the field a name.": "Gefðu sviðinu nafn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ er innbyggt svið, ekki viðbótarsvið — veldu annað nafn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Atriði", + "No recent activity": "Engin nýleg virkni", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Uppfæra", + "No secrets found": "Engin leyndarmál fundust", + "Add your first secret using the button above": "Bættu við fyrsta leyndarmálinu þínu með hnappnum að ofan", + "More actions": "Fleiri aðgerðir", + "New vault": "Ný hvelfing", + "Filter by type": "Sía eftir tegund", + "Vaults": "Hvelfingar", + "All vaults": "Allar hvelfingar", + "Admin settings": "Stjórnunarstillingar", + "Opens in a new tab": "Opnast í nýjum flipa", + "Keepiq navigation": "Keepiq-leiðsögn", + "Vault name": "Nafn hvelfingar", + "Create vault": "Búa til hvelfingu" }, "plurals": null } diff --git a/l10n/it.js b/l10n/it.js index 1475f6146..77431fa97 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Rimuovi questo campo", "Give the field a name.": "Dai un nome al campo.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» è un campo integrato, non aggiuntivo: scegli un altro nome.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elemento", + "No recent activity": "Nessuna attività recente", + "View all": "Vedi tutti", + "Applications awaiting approval": "Applicazioni in attesa di approvazione", + "No applications awaiting approval": "Nessuna applicazione in attesa di approvazione", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Aggiorna", + "No secrets found": "Nessun segreto trovato", + "Add your first secret using the button above": "Aggiungi il tuo primo segreto usando il pulsante qui sopra", + "More actions": "Altre azioni", + "New vault": "Nuova cassaforte", + "Filter by type": "Filtra per tipo", + "Vaults": "Casseforti", + "All vaults": "Tutte le casseforti", + "Admin settings": "Impostazioni di amministrazione", + "Opens in a new tab": "Si apre in una nuova scheda", + "Keepiq navigation": "Navigazione di Keepiq", + "Vault name": "Nome della cassaforte", + "Create vault": "Crea cassaforte" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/it.json b/l10n/it.json index 6b53a5096..fffde4358 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -426,7 +426,28 @@ "Remove this field": "Rimuovi questo campo", "Give the field a name.": "Dai un nome al campo.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» è un campo integrato, non aggiuntivo: scegli un altro nome.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elemento", + "No recent activity": "Nessuna attività recente", + "View all": "Vedi tutti", + "Applications awaiting approval": "Applicazioni in attesa di approvazione", + "No applications awaiting approval": "Nessuna applicazione in attesa di approvazione", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Aggiorna", + "No secrets found": "Nessun segreto trovato", + "Add your first secret using the button above": "Aggiungi il tuo primo segreto usando il pulsante qui sopra", + "More actions": "Altre azioni", + "New vault": "Nuova cassaforte", + "Filter by type": "Filtra per tipo", + "Vaults": "Casseforti", + "All vaults": "Tutte le casseforti", + "Admin settings": "Impostazioni di amministrazione", + "Opens in a new tab": "Si apre in una nuova scheda", + "Keepiq navigation": "Navigazione di Keepiq", + "Vault name": "Nome della cassaforte", + "Create vault": "Crea cassaforte" }, "plurals": null } diff --git a/l10n/lb.js b/l10n/lb.js index 1dec0f851..b709e3e7a 100644 --- a/l10n/lb.js +++ b/l10n/lb.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Dëst Feld ewechhuelen", "Give the field a name.": "Gitt dem Feld en Numm.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ ass en integréiert Feld, kee zousätzlecht — wielt en anere Numm.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Keng rezent Aktivitéit", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Aktualiséieren", + "No secrets found": "Keng Geheimnisser fonnt", + "Add your first secret using the button above": "Füügt Äert éischt Geheimnis mam Knäppchen uewen dobäi", + "More actions": "Méi Aktiounen", + "New vault": "Neien Tresor", + "Filter by type": "No Typ filteren", + "Vaults": "Tresore", + "All vaults": "All Tresore", + "Admin settings": "Administratiounsastellungen", + "Opens in a new tab": "Gëtt an engem neien Tab opgemaach", + "Keepiq navigation": "Keepiq-Navigatioun", + "Vault name": "Tresorsnumm", + "Create vault": "Tresor uleeën" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/lb.json b/l10n/lb.json index d84780315..6e68ab344 100644 --- a/l10n/lb.json +++ b/l10n/lb.json @@ -426,7 +426,28 @@ "Remove this field": "Dëst Feld ewechhuelen", "Give the field a name.": "Gitt dem Feld en Numm.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ ass en integréiert Feld, kee zousätzlecht — wielt en anere Numm.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Keng rezent Aktivitéit", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Aktualiséieren", + "No secrets found": "Keng Geheimnisser fonnt", + "Add your first secret using the button above": "Füügt Äert éischt Geheimnis mam Knäppchen uewen dobäi", + "More actions": "Méi Aktiounen", + "New vault": "Neien Tresor", + "Filter by type": "No Typ filteren", + "Vaults": "Tresore", + "All vaults": "All Tresore", + "Admin settings": "Administratiounsastellungen", + "Opens in a new tab": "Gëtt an engem neien Tab opgemaach", + "Keepiq navigation": "Keepiq-Navigatioun", + "Vault name": "Tresorsnumm", + "Create vault": "Tresor uleeën" }, "plurals": null } diff --git a/l10n/lt.js b/l10n/lt.js index ce08dcd30..bd46d0371 100644 --- a/l10n/lt.js +++ b/l10n/lt.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Pašalinti šį lauką", "Give the field a name.": "Suteikite laukui pavadinimą.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ yra integruotas laukas, o ne papildomas — pasirinkite kitą pavadinimą.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elementas", + "No recent activity": "Nėra naujausios veiklos", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Atnaujinti", + "No secrets found": "Paslapčių nerasta", + "Add your first secret using the button above": "Pridėkite savo pirmąją paslaptį mygtuku viršuje", + "More actions": "Daugiau veiksmų", + "New vault": "Nauja saugykla", + "Filter by type": "Filtruoti pagal tipą", + "Vaults": "Saugyklos", + "All vaults": "Visos saugyklos", + "Admin settings": "Administravimo nustatymai", + "Opens in a new tab": "Atsidaro naujoje kortelėje", + "Keepiq navigation": "Keepiq naršymas", + "Vault name": "Saugyklos pavadinimas", + "Create vault": "Sukurti saugyklą" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/lt.json b/l10n/lt.json index 4a198eb3f..307564d30 100644 --- a/l10n/lt.json +++ b/l10n/lt.json @@ -426,7 +426,28 @@ "Remove this field": "Pašalinti šį lauką", "Give the field a name.": "Suteikite laukui pavadinimą.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ yra integruotas laukas, o ne papildomas — pasirinkite kitą pavadinimą.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Elementas", + "No recent activity": "Nėra naujausios veiklos", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Atnaujinti", + "No secrets found": "Paslapčių nerasta", + "Add your first secret using the button above": "Pridėkite savo pirmąją paslaptį mygtuku viršuje", + "More actions": "Daugiau veiksmų", + "New vault": "Nauja saugykla", + "Filter by type": "Filtruoti pagal tipą", + "Vaults": "Saugyklos", + "All vaults": "Visos saugyklos", + "Admin settings": "Administravimo nustatymai", + "Opens in a new tab": "Atsidaro naujoje kortelėje", + "Keepiq navigation": "Keepiq naršymas", + "Vault name": "Saugyklos pavadinimas", + "Create vault": "Sukurti saugyklą" }, "plurals": null } diff --git a/l10n/lv.js b/l10n/lv.js index 8b81dff2a..e03a1098f 100644 --- a/l10n/lv.js +++ b/l10n/lv.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Noņemt šo lauku", "Give the field a name.": "Piešķir laukam nosaukumu.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” ir iebūvēts lauks, nevis papildu — izvēlies citu nosaukumu.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Vienums", + "No recent activity": "Nav nesenu darbību", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Atsvaidzināt", + "No secrets found": "Noslēpumi nav atrasti", + "Add your first secret using the button above": "Pievienojiet savu pirmo noslēpumu ar augšējo pogu", + "More actions": "Vairāk darbību", + "New vault": "Jauna glabātava", + "Filter by type": "Filtrēt pēc veida", + "Vaults": "Glabātavas", + "All vaults": "Visas glabātavas", + "Admin settings": "Administrēšanas iestatījumi", + "Opens in a new tab": "Atveras jaunā cilnē", + "Keepiq navigation": "Keepiq navigācija", + "Vault name": "Glabātavas nosaukums", + "Create vault": "Izveidot glabātavu" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/lv.json b/l10n/lv.json index 2bab5a60d..ab19c565b 100644 --- a/l10n/lv.json +++ b/l10n/lv.json @@ -426,7 +426,28 @@ "Remove this field": "Noņemt šo lauku", "Give the field a name.": "Piešķir laukam nosaukumu.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” ir iebūvēts lauks, nevis papildu — izvēlies citu nosaukumu.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Vienums", + "No recent activity": "Nav nesenu darbību", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Atsvaidzināt", + "No secrets found": "Noslēpumi nav atrasti", + "Add your first secret using the button above": "Pievienojiet savu pirmo noslēpumu ar augšējo pogu", + "More actions": "Vairāk darbību", + "New vault": "Jauna glabātava", + "Filter by type": "Filtrēt pēc veida", + "Vaults": "Glabātavas", + "All vaults": "Visas glabātavas", + "Admin settings": "Administrēšanas iestatījumi", + "Opens in a new tab": "Atveras jaunā cilnē", + "Keepiq navigation": "Keepiq navigācija", + "Vault name": "Glabātavas nosaukums", + "Create vault": "Izveidot glabātavu" }, "plurals": null } diff --git a/l10n/mk.js b/l10n/mk.js index cb0ffabe3..19e1c884b 100644 --- a/l10n/mk.js +++ b/l10n/mk.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Отстрани го ова поле", "Give the field a name.": "Дајте му име на полето.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ е вградено поле, а не дополнително — одберете друго име.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Ставка", + "No recent activity": "Нема неодамнешна активност", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Освежи", + "No secrets found": "Не се пронајдени тајни", + "Add your first secret using the button above": "Додајте ја вашата прва тајна со копчето погоре", + "More actions": "Повеќе дејства", + "New vault": "Нов трезор", + "Filter by type": "Филтрирај по тип", + "Vaults": "Трезори", + "All vaults": "Сите трезори", + "Admin settings": "Административни поставки", + "Opens in a new tab": "Се отвора во ново јазиче", + "Keepiq navigation": "Навигација на Keepiq", + "Vault name": "Име на трезорот", + "Create vault": "Создај трезор" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/mk.json b/l10n/mk.json index 8dc73329b..3ca055a1f 100644 --- a/l10n/mk.json +++ b/l10n/mk.json @@ -426,7 +426,28 @@ "Remove this field": "Отстрани го ова поле", "Give the field a name.": "Дајте му име на полето.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ е вградено поле, а не дополнително — одберете друго име.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Ставка", + "No recent activity": "Нема неодамнешна активност", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Освежи", + "No secrets found": "Не се пронајдени тајни", + "Add your first secret using the button above": "Додајте ја вашата прва тајна со копчето погоре", + "More actions": "Повеќе дејства", + "New vault": "Нов трезор", + "Filter by type": "Филтрирај по тип", + "Vaults": "Трезори", + "All vaults": "Сите трезори", + "Admin settings": "Административни поставки", + "Opens in a new tab": "Се отвора во ново јазиче", + "Keepiq navigation": "Навигација на Keepiq", + "Vault name": "Име на трезорот", + "Create vault": "Создај трезор" }, "plurals": null } diff --git a/l10n/mt.js b/l10n/mt.js index 72daa9103..266395787 100644 --- a/l10n/mt.js +++ b/l10n/mt.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Neħħi dan il-qasam", "Give the field a name.": "Agħti isem lill-qasam.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” huwa qasam integrat, mhux addizzjonali — agħżel isem oħra.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "L-ebda attività riċenti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Aġġorna", + "No secrets found": "Ma nstabu l-ebda sigrieti", + "Add your first secret using the button above": "Żid l-ewwel sigriet tiegħek bil-buttuna ta’ fuq", + "More actions": "Aktar azzjonijiet", + "New vault": "Kaxxa-forti ġdida", + "Filter by type": "Iffiltra skont it-tip", + "Vaults": "Kaxxi-forti", + "All vaults": "Il-kaxxi-forti kollha", + "Admin settings": "Settings tal-amministrazzjoni", + "Opens in a new tab": "Jinfetaħ f’tab ġdida", + "Keepiq navigation": "Navigazzjoni ta’ Keepiq", + "Vault name": "Isem il-kaxxa-forti", + "Create vault": "Oħloq kaxxa-forti" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/mt.json b/l10n/mt.json index 87a17c707..a4a6fc2d6 100644 --- a/l10n/mt.json +++ b/l10n/mt.json @@ -426,7 +426,28 @@ "Remove this field": "Neħħi dan il-qasam", "Give the field a name.": "Agħti isem lill-qasam.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” huwa qasam integrat, mhux addizzjonali — agħżel isem oħra.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "L-ebda attività riċenti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Aġġorna", + "No secrets found": "Ma nstabu l-ebda sigrieti", + "Add your first secret using the button above": "Żid l-ewwel sigriet tiegħek bil-buttuna ta’ fuq", + "More actions": "Aktar azzjonijiet", + "New vault": "Kaxxa-forti ġdida", + "Filter by type": "Iffiltra skont it-tip", + "Vaults": "Kaxxi-forti", + "All vaults": "Il-kaxxi-forti kollha", + "Admin settings": "Settings tal-amministrazzjoni", + "Opens in a new tab": "Jinfetaħ f’tab ġdida", + "Keepiq navigation": "Navigazzjoni ta’ Keepiq", + "Vault name": "Isem il-kaxxa-forti", + "Create vault": "Oħloq kaxxa-forti" }, "plurals": null } diff --git a/l10n/nb.js b/l10n/nb.js index ab4bfedd7..a3614e037 100644 --- a/l10n/nb.js +++ b/l10n/nb.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Fjern dette feltet", "Give the field a name.": "Gi feltet et navn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» er et innebygd felt, ikke et tilleggsfelt — velg et annet navn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Ingen nylig aktivitet", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Oppdater", + "No secrets found": "Ingen hemmeligheter funnet", + "Add your first secret using the button above": "Legg til din første hemmelighet med knappen ovenfor", + "More actions": "Flere handlinger", + "New vault": "Nytt hvelv", + "Filter by type": "Filtrer etter type", + "Vaults": "Hvelv", + "All vaults": "Alle hvelv", + "Admin settings": "Administrasjonsinnstillinger", + "Opens in a new tab": "Åpnes i en ny fane", + "Keepiq navigation": "Keepiq-navigasjon", + "Vault name": "Hvelvnavn", + "Create vault": "Opprett hvelv" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nb.json b/l10n/nb.json index 38f8d0d4a..3e26005ef 100644 --- a/l10n/nb.json +++ b/l10n/nb.json @@ -426,7 +426,28 @@ "Remove this field": "Fjern dette feltet", "Give the field a name.": "Gi feltet et navn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» er et innebygd felt, ikke et tilleggsfelt — velg et annet navn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Ingen nylig aktivitet", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Oppdater", + "No secrets found": "Ingen hemmeligheter funnet", + "Add your first secret using the button above": "Legg til din første hemmelighet med knappen ovenfor", + "More actions": "Flere handlinger", + "New vault": "Nytt hvelv", + "Filter by type": "Filtrer etter type", + "Vaults": "Hvelv", + "All vaults": "Alle hvelv", + "Admin settings": "Administrasjonsinnstillinger", + "Opens in a new tab": "Åpnes i en ny fane", + "Keepiq navigation": "Keepiq-navigasjon", + "Vault name": "Hvelvnavn", + "Create vault": "Opprett hvelv" }, "plurals": null } diff --git a/l10n/nl.js b/l10n/nl.js index fd7a21f99..6a6880c35 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1,6 +1,18 @@ OC.L10N.register( "keepiq", { + "Getting started": "Aan de slag", + "A short tour of the main screens. It takes under a minute, and you can close it at any point and pick it up again from the help menu.": "Een korte rondleiding langs de belangrijkste schermen. Het duurt minder dan een minuut en je kunt op elk moment stoppen en later verder gaan via het helpmenu.", + "Open Dashboard from the menu.": "Open Dashboard via het menu.", + "Click Dashboard in the menu": "Klik op Dashboard in het menu", + "Open All secrets from the menu.": "Open All secrets via het menu.", + "Click All secrets in the menu": "Klik op All secrets in het menu", + "Open Features & roadmap from the menu.": "Open Features & roadmap via het menu.", + "Click Features & roadmap in the menu": "Klik op Features & roadmap in het menu", + "Open My activity from the menu.": "Open My activity via het menu.", + "Click My activity in the menu": "Klik op My activity in het menu", + "Flows automate the repetitive parts of this app. Have a look, though you do not have to build one now.": "Flows automatiseren het repeterende werk in deze app. Kijk gerust rond, je hoeft er nu geen te maken.", + "Open Flows from the menu": "Open Flows via het menu", "Take over as vault administrator": "Overnemen als kluisbeheerder", "Select {name}": "{name} selecteren", "Could not load the password policy.": "Kan het wachtwoordbeleid niet laden.", @@ -411,7 +423,7 @@ OC.L10N.register( "Unlock the vault before creating a secret.": "Ontgrendel de kluis voordat u een geheim aanmaakt.", "URL (optional)": "URL (optioneel)", "Used {used} of {limit}": "{used} van {limit} gebruikt", - "Vault root": "Kluiswortel", + "Vault root": "Hoofdmap", "via group": "via groep", "Welcome to Keepiq": "Welkom bij Keepiq", "Widgets": "Widgets", @@ -587,6 +599,24 @@ OC.L10N.register( "Give the field a name.": "Geef het veld een naam.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” is een ingebouwd veld, geen extra veld — kies een andere naam.", "Keepiq {version}": "Keepiq {version}", + "Item": "Item", + "No recent activity": "Geen recente activiteit", + "View all": "Alles bekijken", + "Applications awaiting approval": "Applicaties in afwachting van goedkeuring", + "No applications awaiting approval": "Geen applicaties in afwachting van goedkeuring", + "Add your first secret using the button above": "Voeg uw eerste geheim toe met de knop hierboven", + "Filter by type": "Filteren op type", + "More actions": "Meer acties", + "New vault": "Nieuwe kluis", + "No secrets found": "Geen geheimen gevonden", + "Refresh": "Vernieuwen", + "Vaults": "Kluizen", + "All vaults": "Alle kluizen", + "Admin settings": "Beheerdersinstellingen", + "Opens in a new tab": "Opent in een nieuw tabblad", + "Keepiq navigation": "Keepiq-navigatie", + "Vault name": "Kluisnaam", + "Create vault": "Kluis aanmaken", "Applications": "Applicaties", "Features & roadmap": "Functies en roadmap", "Certificates": "Certificaten", @@ -597,7 +627,11 @@ OC.L10N.register( "Your secrets, folders and applications at a glance": "Je geheimen, mappen en applicaties in één oogopslag", "Rotation due": "Rotatie verlopen", "Application": "Applicatie", - "Secure message": "Beveiligd bericht" + "Secure message": "Beveiligd bericht", + "Flows": "Flows", + "Flow": "Flow", + "Registered by": "Geregistreerd door", + "Requested": "Aangevraagd" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index 63a456ca5..951f6716d 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1,5 +1,17 @@ { "translations": { + "Getting started": "Aan de slag", + "A short tour of the main screens. It takes under a minute, and you can close it at any point and pick it up again from the help menu.": "Een korte rondleiding langs de belangrijkste schermen. Het duurt minder dan een minuut en je kunt op elk moment stoppen en later verder gaan via het helpmenu.", + "Open Dashboard from the menu.": "Open Dashboard via het menu.", + "Click Dashboard in the menu": "Klik op Dashboard in het menu", + "Open All secrets from the menu.": "Open All secrets via het menu.", + "Click All secrets in the menu": "Klik op All secrets in het menu", + "Open Features & roadmap from the menu.": "Open Features & roadmap via het menu.", + "Click Features & roadmap in the menu": "Klik op Features & roadmap in het menu", + "Open My activity from the menu.": "Open My activity via het menu.", + "Click My activity in the menu": "Klik op My activity in het menu", + "Flows automate the repetitive parts of this app. Have a look, though you do not have to build one now.": "Flows automatiseren het repeterende werk in deze app. Kijk gerust rond, je hoeft er nu geen te maken.", + "Open Flows from the menu": "Open Flows via het menu", "Take over as vault administrator": "Overnemen als kluisbeheerder", "Select {name}": "{name} selecteren", "Could not load the password policy.": "Kan het wachtwoordbeleid niet laden.", @@ -410,7 +422,7 @@ "Unlock the vault before creating a secret.": "Ontgrendel de kluis voordat u een geheim aanmaakt.", "URL (optional)": "URL (optioneel)", "Used {used} of {limit}": "{used} van {limit} gebruikt", - "Vault root": "Kluiswortel", + "Vault root": "Hoofdmap", "via group": "via groep", "Welcome to Keepiq": "Welkom bij Keepiq", "Widgets": "Widgets", @@ -586,6 +598,24 @@ "Give the field a name.": "Geef het veld een naam.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” is een ingebouwd veld, geen extra veld — kies een andere naam.", "Keepiq {version}": "Keepiq {version}", + "Item": "Item", + "No recent activity": "Geen recente activiteit", + "View all": "Alles bekijken", + "Applications awaiting approval": "Applicaties in afwachting van goedkeuring", + "No applications awaiting approval": "Geen applicaties in afwachting van goedkeuring", + "Add your first secret using the button above": "Voeg uw eerste geheim toe met de knop hierboven", + "Filter by type": "Filteren op type", + "More actions": "Meer acties", + "New vault": "Nieuwe kluis", + "No secrets found": "Geen geheimen gevonden", + "Refresh": "Vernieuwen", + "Vaults": "Kluizen", + "All vaults": "Alle kluizen", + "Admin settings": "Beheerdersinstellingen", + "Opens in a new tab": "Opent in een nieuw tabblad", + "Keepiq navigation": "Keepiq-navigatie", + "Vault name": "Kluisnaam", + "Create vault": "Kluis aanmaken", "Applications": "Applicaties", "Features & roadmap": "Functies en roadmap", "Certificates": "Certificaten", @@ -596,7 +626,11 @@ "Your secrets, folders and applications at a glance": "Je geheimen, mappen en applicaties in één oogopslag", "Rotation due": "Rotatie verlopen", "Application": "Applicatie", - "Secure message": "Beveiligd bericht" + "Secure message": "Beveiligd bericht", + "Flows": "Flows", + "Flow": "Flow", + "Registered by": "Geregistreerd door", + "Requested": "Aangevraagd" }, "plurals": null, "pluralForm": "nplurals=2; plural=(n != 1);" diff --git a/l10n/pl.js b/l10n/pl.js index eb286d6cf..521900809 100644 --- a/l10n/pl.js +++ b/l10n/pl.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Usuń to pole", "Give the field a name.": "Nadaj polu nazwę.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}” to pole wbudowane, a nie dodatkowe — wybierz inną nazwę.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Brak ostatniej aktywności", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Odśwież", + "No secrets found": "Nie znaleziono sekretów", + "Add your first secret using the button above": "Dodaj swój pierwszy sekret przyciskiem powyżej", + "More actions": "Więcej działań", + "New vault": "Nowy sejf", + "Filter by type": "Filtruj według typu", + "Vaults": "Sejfy", + "All vaults": "Wszystkie sejfy", + "Admin settings": "Ustawienia administracyjne", + "Opens in a new tab": "Otwiera się w nowej karcie", + "Keepiq navigation": "Nawigacja Keepiq", + "Vault name": "Nazwa sejfu", + "Create vault": "Utwórz sejf" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/pl.json b/l10n/pl.json index c5b5f2c02..818501ce8 100644 --- a/l10n/pl.json +++ b/l10n/pl.json @@ -426,7 +426,28 @@ "Remove this field": "Usuń to pole", "Give the field a name.": "Nadaj polu nazwę.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}” to pole wbudowane, a nie dodatkowe — wybierz inną nazwę.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Brak ostatniej aktywności", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Odśwież", + "No secrets found": "Nie znaleziono sekretów", + "Add your first secret using the button above": "Dodaj swój pierwszy sekret przyciskiem powyżej", + "More actions": "Więcej działań", + "New vault": "Nowy sejf", + "Filter by type": "Filtruj według typu", + "Vaults": "Sejfy", + "All vaults": "Wszystkie sejfy", + "Admin settings": "Ustawienia administracyjne", + "Opens in a new tab": "Otwiera się w nowej karcie", + "Keepiq navigation": "Nawigacja Keepiq", + "Vault name": "Nazwa sejfu", + "Create vault": "Utwórz sejf" }, "plurals": null } diff --git a/l10n/pt.js b/l10n/pt.js index 3279beebc..b92e9f925 100644 --- a/l10n/pt.js +++ b/l10n/pt.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Remover este campo", "Give the field a name.": "Dê um nome ao campo.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” é um campo integrado, não adicional — escolha outro nome.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Item", + "No recent activity": "Sem atividade recente", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Atualizar", + "No secrets found": "Nenhum segredo encontrado", + "Add your first secret using the button above": "Adicione o seu primeiro segredo com o botão acima", + "More actions": "Mais ações", + "New vault": "Novo cofre", + "Filter by type": "Filtrar por tipo", + "Vaults": "Cofres", + "All vaults": "Todos os cofres", + "Admin settings": "Definições de administração", + "Opens in a new tab": "Abre num novo separador", + "Keepiq navigation": "Navegação do Keepiq", + "Vault name": "Nome do cofre", + "Create vault": "Criar cofre" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/pt.json b/l10n/pt.json index 2e00a5571..e0dbddc99 100644 --- a/l10n/pt.json +++ b/l10n/pt.json @@ -426,7 +426,28 @@ "Remove this field": "Remover este campo", "Give the field a name.": "Dê um nome ao campo.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” é um campo integrado, não adicional — escolha outro nome.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Item", + "No recent activity": "Sem atividade recente", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Atualizar", + "No secrets found": "Nenhum segredo encontrado", + "Add your first secret using the button above": "Adicione o seu primeiro segredo com o botão acima", + "More actions": "Mais ações", + "New vault": "Novo cofre", + "Filter by type": "Filtrar por tipo", + "Vaults": "Cofres", + "All vaults": "Todos os cofres", + "Admin settings": "Definições de administração", + "Opens in a new tab": "Abre num novo separador", + "Keepiq navigation": "Navegação do Keepiq", + "Vault name": "Nome do cofre", + "Create vault": "Criar cofre" }, "plurals": null } diff --git a/l10n/rm.js b/l10n/rm.js index 929de22e4..3a5bd1ac1 100644 --- a/l10n/rm.js +++ b/l10n/rm.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Allontanar quest champ", "Give the field a name.": "Dai in num al champ.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» è in champ integrà, betg in supplementar — tscherna in auter num.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Nagina activitad recenta", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Actualisar", + "No secrets found": "Nagins secrets chattads", + "Add your first secret using the button above": "Agiunschè voss emprim secret cun il buttun survart", + "More actions": "Dapli acziuns", + "New vault": "Nov tresor", + "Filter by type": "Filtrar tenor tip", + "Vaults": "Tresors", + "All vaults": "Tut ils tresors", + "Admin settings": "Parameters d’administraziun", + "Opens in a new tab": "S’avra en in nov tab", + "Keepiq navigation": "Navigaziun da Keepiq", + "Vault name": "Num dal tresor", + "Create vault": "Crear tresor" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/rm.json b/l10n/rm.json index b019af8b1..b9250a664 100644 --- a/l10n/rm.json +++ b/l10n/rm.json @@ -426,7 +426,28 @@ "Remove this field": "Allontanar quest champ", "Give the field a name.": "Dai in num al champ.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» è in champ integrà, betg in supplementar — tscherna in auter num.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Nagina activitad recenta", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Actualisar", + "No secrets found": "Nagins secrets chattads", + "Add your first secret using the button above": "Agiunschè voss emprim secret cun il buttun survart", + "More actions": "Dapli acziuns", + "New vault": "Nov tresor", + "Filter by type": "Filtrar tenor tip", + "Vaults": "Tresors", + "All vaults": "Tut ils tresors", + "Admin settings": "Parameters d’administraziun", + "Opens in a new tab": "S’avra en in nov tab", + "Keepiq navigation": "Navigaziun da Keepiq", + "Vault name": "Num dal tresor", + "Create vault": "Crear tresor" }, "plurals": null } diff --git a/l10n/ro.js b/l10n/ro.js index 35f96149d..44d83cf73 100644 --- a/l10n/ro.js +++ b/l10n/ro.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Eliminați acest câmp", "Give the field a name.": "Dați un nume câmpului.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}” este un câmp integrat, nu suplimentar — alegeți alt nume.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Nicio activitate recentă", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Reîmprospătare", + "No secrets found": "Nu s-au găsit secrete", + "Add your first secret using the button above": "Adăugați primul secret cu butonul de mai sus", + "More actions": "Mai multe acțiuni", + "New vault": "Seif nou", + "Filter by type": "Filtrare după tip", + "Vaults": "Seifuri", + "All vaults": "Toate seifurile", + "Admin settings": "Setări de administrare", + "Opens in a new tab": "Se deschide într-o filă nouă", + "Keepiq navigation": "Navigare Keepiq", + "Vault name": "Numele seifului", + "Create vault": "Creare seif" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ro.json b/l10n/ro.json index 9d3d47902..cbbdef951 100644 --- a/l10n/ro.json +++ b/l10n/ro.json @@ -426,7 +426,28 @@ "Remove this field": "Eliminați acest câmp", "Give the field a name.": "Dați un nume câmpului.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}” este un câmp integrat, nu suplimentar — alegeți alt nume.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Nicio activitate recentă", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Reîmprospătare", + "No secrets found": "Nu s-au găsit secrete", + "Add your first secret using the button above": "Adăugați primul secret cu butonul de mai sus", + "More actions": "Mai multe acțiuni", + "New vault": "Seif nou", + "Filter by type": "Filtrare după tip", + "Vaults": "Seifuri", + "All vaults": "Toate seifurile", + "Admin settings": "Setări de administrare", + "Opens in a new tab": "Se deschide într-o filă nouă", + "Keepiq navigation": "Navigare Keepiq", + "Vault name": "Numele seifului", + "Create vault": "Creare seif" }, "plurals": null } diff --git a/l10n/ru.js b/l10n/ru.js index 5032c70b3..36ed5c3f3 100644 --- a/l10n/ru.js +++ b/l10n/ru.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Удалить это поле", "Give the field a name.": "Дайте полю имя.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» — встроенное поле, а не дополнительное: выберите другое имя.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Элемент", + "No recent activity": "Нет недавней активности", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Обновить", + "No secrets found": "Секреты не найдены", + "Add your first secret using the button above": "Добавьте свой первый секрет кнопкой выше", + "More actions": "Больше действий", + "New vault": "Новое хранилище", + "Filter by type": "Фильтровать по типу", + "Vaults": "Хранилища", + "All vaults": "Все хранилища", + "Admin settings": "Настройки администратора", + "Opens in a new tab": "Открывается в новой вкладке", + "Keepiq navigation": "Навигация Keepiq", + "Vault name": "Название хранилища", + "Create vault": "Создать хранилище" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ru.json b/l10n/ru.json index 0fefbf104..df9ea195a 100644 --- a/l10n/ru.json +++ b/l10n/ru.json @@ -426,7 +426,28 @@ "Remove this field": "Удалить это поле", "Give the field a name.": "Дайте полю имя.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» — встроенное поле, а не дополнительное: выберите другое имя.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Элемент", + "No recent activity": "Нет недавней активности", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Обновить", + "No secrets found": "Секреты не найдены", + "Add your first secret using the button above": "Добавьте свой первый секрет кнопкой выше", + "More actions": "Больше действий", + "New vault": "Новое хранилище", + "Filter by type": "Фильтровать по типу", + "Vaults": "Хранилища", + "All vaults": "Все хранилища", + "Admin settings": "Настройки администратора", + "Opens in a new tab": "Открывается в новой вкладке", + "Keepiq navigation": "Навигация Keepiq", + "Vault name": "Название хранилища", + "Create vault": "Создать хранилище" }, "plurals": null } diff --git a/l10n/sk.js b/l10n/sk.js index 1d6f14224..f8b588644 100644 --- a/l10n/sk.js +++ b/l10n/sk.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Odobrať toto pole", "Give the field a name.": "Pomenujte pole.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je vstavané pole, nie ďalšie — zvoľte iný názov.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Položka", + "No recent activity": "Žiadna nedávna aktivita", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Obnoviť", + "No secrets found": "Nenašli sa žiadne tajomstvá", + "Add your first secret using the button above": "Pridajte svoje prvé tajomstvo tlačidlom vyššie", + "More actions": "Ďalšie akcie", + "New vault": "Nový trezor", + "Filter by type": "Filtrovať podľa typu", + "Vaults": "Trezory", + "All vaults": "Všetky trezory", + "Admin settings": "Nastavenia správy", + "Opens in a new tab": "Otvorí sa na novej karte", + "Keepiq navigation": "Navigácia Keepiq", + "Vault name": "Názov trezoru", + "Create vault": "Vytvoriť trezor" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sk.json b/l10n/sk.json index daa83d6fc..bf3705098 100644 --- a/l10n/sk.json +++ b/l10n/sk.json @@ -426,7 +426,28 @@ "Remove this field": "Odobrať toto pole", "Give the field a name.": "Pomenujte pole.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je vstavané pole, nie ďalšie — zvoľte iný názov.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Položka", + "No recent activity": "Žiadna nedávna aktivita", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Obnoviť", + "No secrets found": "Nenašli sa žiadne tajomstvá", + "Add your first secret using the button above": "Pridajte svoje prvé tajomstvo tlačidlom vyššie", + "More actions": "Ďalšie akcie", + "New vault": "Nový trezor", + "Filter by type": "Filtrovať podľa typu", + "Vaults": "Trezory", + "All vaults": "Všetky trezory", + "Admin settings": "Nastavenia správy", + "Opens in a new tab": "Otvorí sa na novej karte", + "Keepiq navigation": "Navigácia Keepiq", + "Vault name": "Názov trezoru", + "Create vault": "Vytvoriť trezor" }, "plurals": null } diff --git a/l10n/sl.js b/l10n/sl.js index 6201ff739..37938c350 100644 --- a/l10n/sl.js +++ b/l10n/sl.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Odstrani to polje", "Give the field a name.": "Poimenujte polje.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je vgrajeno polje, ne dodatno — izberite drugo ime.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Ni nedavne dejavnosti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osveži", + "No secrets found": "Ni najdenih skrivnosti", + "Add your first secret using the button above": "Dodajte svojo prvo skrivnost z gumbom zgoraj", + "More actions": "Več dejanj", + "New vault": "Nov trezor", + "Filter by type": "Filtriraj po vrsti", + "Vaults": "Trezorji", + "All vaults": "Vsi trezorji", + "Admin settings": "Skrbniške nastavitve", + "Opens in a new tab": "Odpre se v novem zavihku", + "Keepiq navigation": "Krmarjenje Keepiq", + "Vault name": "Ime trezorja", + "Create vault": "Ustvari trezor" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sl.json b/l10n/sl.json index 61a230df7..a93df117a 100644 --- a/l10n/sl.json +++ b/l10n/sl.json @@ -426,7 +426,28 @@ "Remove this field": "Odstrani to polje", "Give the field a name.": "Poimenujte polje.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ je vgrajeno polje, ne dodatno — izberite drugo ime.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Element", + "No recent activity": "Ni nedavne dejavnosti", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osveži", + "No secrets found": "Ni najdenih skrivnosti", + "Add your first secret using the button above": "Dodajte svojo prvo skrivnost z gumbom zgoraj", + "More actions": "Več dejanj", + "New vault": "Nov trezor", + "Filter by type": "Filtriraj po vrsti", + "Vaults": "Trezorji", + "All vaults": "Vsi trezorji", + "Admin settings": "Skrbniške nastavitve", + "Opens in a new tab": "Odpre se v novem zavihku", + "Keepiq navigation": "Krmarjenje Keepiq", + "Vault name": "Ime trezorja", + "Create vault": "Ustvari trezor" }, "plurals": null } diff --git a/l10n/sq.js b/l10n/sq.js index 6a4371d54..e2a8df0bf 100644 --- a/l10n/sq.js +++ b/l10n/sq.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Hiq këtë fushë", "Give the field a name.": "Jepni emër fushës.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” është fushë e integruar, jo shtesë — zgjidhni emër tjetër.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Artikull", + "No recent activity": "Asnjë aktivitet i fundit", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Rifresko", + "No secrets found": "Nuk u gjetën sekrete", + "Add your first secret using the button above": "Shtoni sekretin tuaj të parë me butonin sipër", + "More actions": "Më shumë veprime", + "New vault": "Kasafortë e re", + "Filter by type": "Filtro sipas llojit", + "Vaults": "Kasaforta", + "All vaults": "Të gjitha kasafortat", + "Admin settings": "Cilësimet e administrimit", + "Opens in a new tab": "Hapet në një skedë të re", + "Keepiq navigation": "Navigimi i Keepiq", + "Vault name": "Emri i kasafortës", + "Create vault": "Krijo kasafortë" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sq.json b/l10n/sq.json index 3420e1674..1b152b67f 100644 --- a/l10n/sq.json +++ b/l10n/sq.json @@ -426,7 +426,28 @@ "Remove this field": "Hiq këtë fushë", "Give the field a name.": "Jepni emër fushës.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” është fushë e integruar, jo shtesë — zgjidhni emër tjetër.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Artikull", + "No recent activity": "Asnjë aktivitet i fundit", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Rifresko", + "No secrets found": "Nuk u gjetën sekrete", + "Add your first secret using the button above": "Shtoni sekretin tuaj të parë me butonin sipër", + "More actions": "Më shumë veprime", + "New vault": "Kasafortë e re", + "Filter by type": "Filtro sipas llojit", + "Vaults": "Kasaforta", + "All vaults": "Të gjitha kasafortat", + "Admin settings": "Cilësimet e administrimit", + "Opens in a new tab": "Hapet në një skedë të re", + "Keepiq navigation": "Navigimi i Keepiq", + "Vault name": "Emri i kasafortës", + "Create vault": "Krijo kasafortë" }, "plurals": null } diff --git a/l10n/sr.js b/l10n/sr.js index 1019c0344..cb246b68b 100644 --- a/l10n/sr.js +++ b/l10n/sr.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Уклони ово поље", "Give the field a name.": "Дајте полју назив.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ је уграђено поље, а не додатно — одаберите други назив.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Ставка", + "No recent activity": "Нема недавних активности", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osveži", + "No secrets found": "Nije pronađena nijedna tajna", + "Add your first secret using the button above": "Dodajte svoju prvu tajnu dugmetom iznad", + "More actions": "Još radnji", + "New vault": "Novi trezor", + "Filter by type": "Filtriraj po tipu", + "Vaults": "Trezori", + "All vaults": "Svi trezori", + "Admin settings": "Administratorska podešavanja", + "Opens in a new tab": "Otvara se u novoj kartici", + "Keepiq navigation": "Keepiq navigacija", + "Vault name": "Naziv trezora", + "Create vault": "Napravi trezor" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sr.json b/l10n/sr.json index fb7141581..167c7b4c0 100644 --- a/l10n/sr.json +++ b/l10n/sr.json @@ -426,7 +426,28 @@ "Remove this field": "Уклони ово поље", "Give the field a name.": "Дајте полју назив.", "“{name}” is a built-in field, not an additional one — choose a different name.": "„{name}“ је уграђено поље, а не додатно — одаберите други назив.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Ставка", + "No recent activity": "Нема недавних активности", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Osveži", + "No secrets found": "Nije pronađena nijedna tajna", + "Add your first secret using the button above": "Dodajte svoju prvu tajnu dugmetom iznad", + "More actions": "Još radnji", + "New vault": "Novi trezor", + "Filter by type": "Filtriraj po tipu", + "Vaults": "Trezori", + "All vaults": "Svi trezori", + "Admin settings": "Administratorska podešavanja", + "Opens in a new tab": "Otvara se u novoj kartici", + "Keepiq navigation": "Keepiq navigacija", + "Vault name": "Naziv trezora", + "Create vault": "Napravi trezor" }, "plurals": null } diff --git a/l10n/sv.js b/l10n/sv.js index e02c76dfc..d55b1478d 100644 --- a/l10n/sv.js +++ b/l10n/sv.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Ta bort det här fältet", "Give the field a name.": "Ge fältet ett namn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "”{name}” är ett inbyggt fält, inte ett ytterligare — välj ett annat namn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Objekt", + "No recent activity": "Ingen ny aktivitet", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Uppdatera", + "No secrets found": "Inga hemligheter hittades", + "Add your first secret using the button above": "Lägg till din första hemlighet med knappen ovan", + "More actions": "Fler åtgärder", + "New vault": "Nytt valv", + "Filter by type": "Filtrera efter typ", + "Vaults": "Valv", + "All vaults": "Alla valv", + "Admin settings": "Administrationsinställningar", + "Opens in a new tab": "Öppnas i en ny flik", + "Keepiq navigation": "Keepiq-navigering", + "Vault name": "Valvets namn", + "Create vault": "Skapa valv" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sv.json b/l10n/sv.json index b597de4e8..4e569a5b0 100644 --- a/l10n/sv.json +++ b/l10n/sv.json @@ -426,7 +426,28 @@ "Remove this field": "Ta bort det här fältet", "Give the field a name.": "Ge fältet ett namn.", "“{name}” is a built-in field, not an additional one — choose a different name.": "”{name}” är ett inbyggt fält, inte ett ytterligare — välj ett annat namn.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Objekt", + "No recent activity": "Ingen ny aktivitet", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Uppdatera", + "No secrets found": "Inga hemligheter hittades", + "Add your first secret using the button above": "Lägg till din första hemlighet med knappen ovan", + "More actions": "Fler åtgärder", + "New vault": "Nytt valv", + "Filter by type": "Filtrera efter typ", + "Vaults": "Valv", + "All vaults": "Alla valv", + "Admin settings": "Administrationsinställningar", + "Opens in a new tab": "Öppnas i en ny flik", + "Keepiq navigation": "Keepiq-navigering", + "Vault name": "Valvets namn", + "Create vault": "Skapa valv" }, "plurals": null } diff --git a/l10n/tr.js b/l10n/tr.js index baa7605d1..9f13cc2a1 100644 --- a/l10n/tr.js +++ b/l10n/tr.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Bu alanı kaldır", "Give the field a name.": "Alana bir ad verin.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” yerleşik bir alandır, ek alan değil — başka bir ad seçin.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Öğe", + "No recent activity": "Yakın zamanda etkinlik yok", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Yenile", + "No secrets found": "Hiç gizli bulunamadı", + "Add your first secret using the button above": "İlk gizlinizi yukarıdaki düğmeyle ekleyin", + "More actions": "Diğer işlemler", + "New vault": "Yeni kasa", + "Filter by type": "Türe göre filtrele", + "Vaults": "Kasalar", + "All vaults": "Tüm kasalar", + "Admin settings": "Yönetici ayarları", + "Opens in a new tab": "Yeni sekmede açılır", + "Keepiq navigation": "Keepiq gezinmesi", + "Vault name": "Kasa adı", + "Create vault": "Kasa oluştur" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/tr.json b/l10n/tr.json index 93e2042f7..d0b2b89ba 100644 --- a/l10n/tr.json +++ b/l10n/tr.json @@ -426,7 +426,28 @@ "Remove this field": "Bu alanı kaldır", "Give the field a name.": "Alana bir ad verin.", "“{name}” is a built-in field, not an additional one — choose a different name.": "“{name}” yerleşik bir alandır, ek alan değil — başka bir ad seçin.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Öğe", + "No recent activity": "Yakın zamanda etkinlik yok", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Yenile", + "No secrets found": "Hiç gizli bulunamadı", + "Add your first secret using the button above": "İlk gizlinizi yukarıdaki düğmeyle ekleyin", + "More actions": "Diğer işlemler", + "New vault": "Yeni kasa", + "Filter by type": "Türe göre filtrele", + "Vaults": "Kasalar", + "All vaults": "Tüm kasalar", + "Admin settings": "Yönetici ayarları", + "Opens in a new tab": "Yeni sekmede açılır", + "Keepiq navigation": "Keepiq gezinmesi", + "Vault name": "Kasa adı", + "Create vault": "Kasa oluştur" }, "plurals": null } diff --git a/l10n/uk.js b/l10n/uk.js index e5f21edaf..58a27986a 100644 --- a/l10n/uk.js +++ b/l10n/uk.js @@ -427,7 +427,28 @@ OC.L10N.register( "Remove this field": "Видалити це поле", "Give the field a name.": "Дайте полю назву.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» — вбудоване поле, а не додаткове: виберіть іншу назву.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Елемент", + "No recent activity": "Немає нещодавньої активності", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Оновити", + "No secrets found": "Секретів не знайдено", + "Add your first secret using the button above": "Додайте свій перший секрет кнопкою вище", + "More actions": "Більше дій", + "New vault": "Нове сховище", + "Filter by type": "Фільтрувати за типом", + "Vaults": "Сховища", + "All vaults": "Усі сховища", + "Admin settings": "Налаштування адміністратора", + "Opens in a new tab": "Відкривається в новій вкладці", + "Keepiq navigation": "Навігація Keepiq", + "Vault name": "Назва сховища", + "Create vault": "Створити сховище" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/uk.json b/l10n/uk.json index 309a04052..2cfce460c 100644 --- a/l10n/uk.json +++ b/l10n/uk.json @@ -426,7 +426,28 @@ "Remove this field": "Видалити це поле", "Give the field a name.": "Дайте полю назву.", "“{name}” is a built-in field, not an additional one — choose a different name.": "«{name}» — вбудоване поле, а не додаткове: виберіть іншу назву.", - "Keepiq {version}": "Keepiq {version}" + "Keepiq {version}": "Keepiq {version}", + "Item": "Елемент", + "No recent activity": "Немає нещодавньої активності", + "View all": "View all", + "Applications awaiting approval": "Applications awaiting approval", + "No applications awaiting approval": "No applications awaiting approval", + "Vault overview": "Vault overview", + "Your secrets, folders and applications at a glance": "Your secrets, folders and applications at a glance", + "Rotation due": "Rotation due", + "Refresh": "Оновити", + "No secrets found": "Секретів не знайдено", + "Add your first secret using the button above": "Додайте свій перший секрет кнопкою вище", + "More actions": "Більше дій", + "New vault": "Нове сховище", + "Filter by type": "Фільтрувати за типом", + "Vaults": "Сховища", + "All vaults": "Усі сховища", + "Admin settings": "Налаштування адміністратора", + "Opens in a new tab": "Відкривається в новій вкладці", + "Keepiq navigation": "Навігація Keepiq", + "Vault name": "Назва сховища", + "Create vault": "Створити сховище" }, "plurals": null } diff --git a/openspec/specs/menu-architecture/spec.md b/openspec/specs/menu-architecture/spec.md index 1e5b36800..7881594b7 100644 --- a/openspec/specs/menu-architecture/spec.md +++ b/openspec/specs/menu-architecture/spec.md @@ -1,8 +1,11 @@ # menu-architecture Specification ## Purpose + TBD - created by archiving change adopt-buildmanifest-menu-layout. Update Purpose after archive. + ## Requirements + ### Requirement: Shared buildManifest pipeline Keepiq SHALL build its effective manifest (pages + menu) via `@conduction/nextcloud-vue`'s `buildManifest(base, fragments, menuLayout)` util. No app-local re-implementation of fragment merging, relocation, removal, or settings-section application logic SHALL exist in `src/main.js` or elsewhere in the app. @@ -30,3 +33,12 @@ Keepiq SHALL declare a `src/menu-layout.json` that places configuration, definit - **WHEN** a user navigates to the Applications route via the foldout entry or a direct deep link - **THEN** the `ApplicationRegister` page MUST render identically to before the relocation — the route itself is unchanged, only its menu placement moved +### Requirement: App navigation renders the manifest menu + +Keepiq SHALL render its left navigation rail from the effective manifest's `menu[]`: entries sorted by `order` (entries without one last), split into the main scrollable section, the footer, and the settings foldout by each entry's `section`. Route entries MUST navigate through the SPA router, external `href` entries MUST render as real anchors (opened in a new tab for scheme-prefixed URLs), the entry matching the current route MUST be marked active, and entries carrying `action: "user-settings"` MUST open the host's settings dialog instead of navigating. + +#### Scenario: App navigation renders + +- **GIVEN** a user with an unlocked vault +- **WHEN** they open Keepiq +- **THEN** the left navigation MUST render the manifest menu entries in their declared sections and order, mark the current route's entry as active, render external entries as real anchors, and open the user-settings dialog — without navigating — for entries whose `action` is `"user-settings"` diff --git a/package-lock.json b/package-lock.json index 2e071bfea..4a803716e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "EUPL-1.2", "dependencies": { - "@conduction/nextcloud-vue": "^2.19.0", + "@conduction/nextcloud-vue": "^2.24.2", "@nextcloud/auth": "^2.6.0", "@nextcloud/axios": "~2.5.2", "@nextcloud/capabilities": "^1.2.1", @@ -22,7 +22,7 @@ "@nextcloud/vue": "^9.9.0", "@vueuse/core": "^14.4.0", "argon2-browser": "^1.18.0", - "dexie": "^4.4.4", + "dexie": "^4.4.5", "dompurify": "^3.4.12", "gridstack": "^12.6.0", "marked": "^12.0.0", @@ -546,9 +546,9 @@ } }, "node_modules/@conduction/nextcloud-vue": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.19.0.tgz", - "integrity": "sha512-gSLl4RZ7hy1e2TBuzYZLiTLBMggRYgfuxFMva59JBhb1EP93GgvEd/HpQcg+zoBL9FJPgJw9ir2OGf9Tore7og==", + "version": "2.24.2", + "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.24.2.tgz", + "integrity": "sha512-BGvyJpRhzvLraZuyRB3ETJ6IESV27XZvL5vdvLd5kEvettFo+QWIo6JcHJhQk9np8QYGHBQ9gRHCp3v6y7Y2gw==", "license": "EUPL-1.2", "dependencies": { "@ckpack/vue-color": "^1.6.0", @@ -610,7 +610,7 @@ "dompurify": "^3.0.0", "eslint": "^8.56.0 || ^9.0.0 || ^10.0.0", "eslint-plugin-vue": "^9.21.0 || ^10.0.0", - "gridstack": "^12.0.0", + "gridstack": "^12.0.0 || ^13.0.0", "marked": "^12.0.0", "pinia": "^2.0.0 || ^3.0.0", "vue": "^3.5.0", @@ -8705,9 +8705,9 @@ } }, "node_modules/dexie": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.4.tgz", - "integrity": "sha512-jIwsYI8Os2hgnqc6O49YwFDKGc5v5QjGx0wPVp543ip1F53VFAKMLthV2pQosQcVTv3eAskTWYspOx195PM0FQ==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.5.tgz", + "integrity": "sha512-wWCHdihT3dmlUSuNhn5mMZDWSpG0suxjAni7YjjiZdzabZcKmy3uNZGZ7AeYYXBoLbpSXX35fikPLkPC44Osiw==", "license": "Apache-2.0" }, "node_modules/diffie-hellman": { diff --git a/package.json b/package.json index a9736ff97..919cb3b03 100644 --- a/package.json +++ b/package.json @@ -2,40 +2,41 @@ "name": "keepiq", "version": "0.1.0", "license": "EUPL-1.2", - "engines": { - "node": "^22.14 || ^24 || >=26", - "npm": "^11.0.0" - }, "scripts": { - "test": "vitest run", - "test:unit": "vitest run", - "test:coverage": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=json-summary --coverage.reporter=text-summary --coverage.include=src/** --coverage.reportsDirectory=coverage-vitest", - "test:coverage-ratchet": "npm run test:coverage && bash tests/coverage-ratchet.sh vitest coverage-vitest/coverage-summary.json", - "test:unit:watch": "vitest", - "test:l10n": "node tests/l10n/check-l10n.js", - "test:l10n:write": "node tests/l10n/check-l10n.js --write", "build": "NODE_ENV=production webpack --config webpack.config.js --progress", "build:extension": "node browser-extension/build.mjs", - "watch:extension": "node browser-extension/build.mjs --watch", + "check:l10n-js": "node scripts/build-l10n-js.js --check", + "check:manifest": "node tests/validate-manifest.js", + "check:schema-l10n": "node scripts/check-schema-l10n.js", "dev": "NODE_ENV=development webpack --config webpack.config.js --progress", - "watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch", + "format": "prettier --check \"**/*.{js,ts,vue,css,scss}\"", + "format:fix": "prettier --write \"**/*.{js,ts,vue,css,scss}\"", + "l10n:build": "node scripts/build-l10n-js.js", "lint": "eslint src", "lint-fix": "npm run lint -- --fix", - "check:schema-l10n": "node scripts/check-schema-l10n.js", - "check:manifest": "node tests/validate-manifest.js", - "l10n:build": "node scripts/build-l10n-js.js", - "check:l10n-js": "node scripts/build-l10n-js.js --check", + "prepare": "git config core.hooksPath .githooks || true", "stylelint": "stylelint \"src/**/*.{vue,scss,css}\"", - "format": "prettier --check \"**/*.{js,ts,vue,css,scss}\"", - "format:fix": "prettier --write \"**/*.{js,ts,vue,css,scss}\"", "stylelint-fix": "stylelint \"src/**/*.vue\" \"src/**/*.scss\" \"src/**/*.css\" --fix", - "prepare": "git config core.hooksPath .githooks || true" + "test": "vitest run", + "test:coverage": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=json-summary --coverage.reporter=text-summary --coverage.include=src/** --coverage.reportsDirectory=coverage-vitest", + "test:coverage-ratchet": "npm run test:coverage && bash tests/coverage-ratchet.sh vitest coverage-vitest/coverage-summary.json", + "test:l10n": "node tests/l10n/check-l10n.js", + "test:l10n:write": "node tests/l10n/check-l10n.js --write", + "test:unit": "vitest run", + "test:unit:watch": "vitest", + "watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch", + "watch:extension": "node browser-extension/build.mjs --watch" }, "browserslist": [ "extends @nextcloud/browserslist-config" ], + "prettier": "@nextcloud/prettier-config", + "overrides": { + "@nextcloud/axios": "~2.5.2", + "libxmljs2": "^0.37.0" + }, "dependencies": { - "@conduction/nextcloud-vue": "^2.19.0", + "@conduction/nextcloud-vue": "^2.24.2", "@nextcloud/auth": "^2.6.0", "@nextcloud/axios": "~2.5.2", "@nextcloud/capabilities": "^1.2.1", @@ -48,7 +49,7 @@ "@nextcloud/vue": "^9.9.0", "@vueuse/core": "^14.4.0", "argon2-browser": "^1.18.0", - "dexie": "^4.4.4", + "dexie": "^4.4.5", "dompurify": "^3.4.12", "gridstack": "^12.6.0", "marked": "^12.0.0", @@ -60,10 +61,6 @@ "vue-router": "^4.5.1", "zxcvbn": "^4.4.2" }, - "overrides": { - "libxmljs2": "^0.37.0", - "@nextcloud/axios": "~2.5.2" - }, "devDependencies": { "@cyclonedx/cyclonedx-npm": "^6.0.0", "@nextcloud/browserslist-config": "^3.0.1", @@ -94,5 +91,8 @@ "webpack": "^5.94.0", "webpack-cli": "^6.0.1" }, - "prettier": "@nextcloud/prettier-config" + "engines": { + "node": "^22.14 || ^24 || >=26", + "npm": "^11.0.0" + } } diff --git a/scripts/coverage-guard.php b/scripts/coverage-guard.php index 4082edf7c..1b27e667b 100755 --- a/scripts/coverage-guard.php +++ b/scripts/coverage-guard.php @@ -5,6 +5,8 @@ * * Usage: * php scripts/coverage-guard.php [--against=] + * php scripts/coverage-guard.php --against= --changed-files= + * php scripts/coverage-guard.php --against= --changed-files= --deletion-neutral * php scripts/coverage-guard.php [--update-baseline] * php scripts/coverage-guard.php --capabilities * @@ -53,13 +55,22 @@ * check that did not run looking exactly like one that passed. The probe turns * that into a loud failure. */ -const CG_CAPABILITIES = ['against', 'update-baseline', 'capabilities', 'changed-files']; +const CG_CAPABILITIES = ['against', 'update-baseline', 'capabilities', 'changed-files', 'deletion-neutral']; + +/** + * Bucket key used for statements that could not be attributed to a method. + * + * It is a real key, present on both sides, so a file that falls back is compared + * exactly as it is today — conservatively, deletions included. It is never a way + * for a file to disappear from the comparison. + */ +const CG_WHOLE_FILE = ''; /** * Sum clover metrics for a named subset of files. * * WHY THIS EXISTS. Comparing the project-wide aggregate made the ratchet fire on - * measurement noise. Measured on keepiq#240 — a pull request whose entire diff + * measurement noise. Measured on doriath#240 — a pull request whose entire diff * was `webpack.config.js`, containing no PHP at all: * * Coverage current: 60.50% (8303/13723 statements) @@ -138,6 +149,242 @@ function cgMeasureFiles(string $file, string $label, array $only): array return [$statements, $covered, $percentage]; } +/** + * Attribute every statement in the named files to the METHOD it sits in. + * + * WHY BY METHOD NAME, AND WHY NOT BY LINE NUMBER. + * + * Clover identifies a statement only by its `num` — the line it sits on — and a + * deletion shifts every line after the cut. On launchpad#128's file, 117 of the + * 254 surviving statements (46%) sit at or after the deletion site, so matching + * base statement `num=310` to head statement `num=310` compares two unrelated + * lines across half the file and returns a confident, meaningless verdict. The + * test suite measures that directly on its fixture: a line-number intersection + * there reports 182/234 head against 189/234 base, i.e. a fabricated regression, + * where method-name attribution reconciles exactly at 183/254 on both sides. + * Method names survive the shift; line numbers do not. + * + * THE SHAPE CLOVER ACTUALLY EMITS, verified against 2 608 file entries in four + * real PHPUnit artifacts from this fleet: + * + * + * + * + * + * ... + * + * + * + * `` elements are FLAT and in document order; a `type="method"` line opens + * a method and every `type="stmt"` line after it belongs to that method until the + * next one. `metrics/@statements` counts the `stmt` lines only — methods are + * counted separately and `elements` is their sum — so summing attributed + * statements reproduces the file metric wherever the line data is complete. + * + * The bucket key is the REPO-RELATIVE path that matched, never the clover + * `name`. The two reports are produced from two different checkouts and their + * absolute paths differ, so keying on `name` would put every head bucket and + * every base bucket in disjoint namespaces and the intersection would be empty — + * which reads as "nothing to compare", i.e. a pass. + * + * TWO DEGENERATE SHAPES, BOTH HANDLED FAIL-CLOSED: + * + * - A file whose metrics declare statements but which carries NO usable line + * data (the `processUncoveredFiles` shape: PHPUnit counts a file it never + * loaded). Attribution would measure it as 0/0 — an invisible pass on exactly + * the file most likely to be untested. Such a file falls back to a single + * CG_WHOLE_FILE bucket carrying its file-level metrics, the fallback is + * printed by name, and cgCollapseFiles() then puts BOTH sides on the same + * footing so the fallback cannot be mistaken for a deletion. + * - Two methods of the same name in one file (two classes in one file, in + * principle). Their statements are SUMMED into one bucket rather than + * disambiguated by ordinal, because ordinals shift when one of them is + * deleted. Summing keeps the bucket in the intersection, so deleting one of a + * same-named pair is still charged against the change. Not seen in any of the + * 2 608 entries surveyed; handled so it cannot become a silent hole. + * + * @param string $file Clover report to read. + * @param string $label Human label for error messages. + * @param array $only Repo-relative paths to include. + * + * @return array{0: array, 1: array, 2: array} + * buckets keyed "::", the repo-relative paths this report + * matched, and the paths that had to fall back to file-level metrics. + */ +function cgAttributeMethods(string $file, string $label, array $only): array +{ + if (file_exists($file) === false) { + fwrite(STDERR, "Error: {$label} clover file not found: {$file}\n"); + exit(CG_INPUT); + } + + $xml = @simplexml_load_file($file); + if ($xml === false) { + fwrite(STDERR, "Error: could not parse {$label} report {$file}\n"); + exit(CG_INPUT); + } + + $buckets = []; + $matched = []; + $fellBack = []; + + foreach ($xml->xpath('//file') as $entry) { + $name = (string) $entry['name']; + if ($name === '') { + continue; + } + + $path = null; + foreach ($only as $wanted) { + if (str_ends_with($name, $wanted) === true) { + $path = $wanted; + break; + } + } + + if ($path === null) { + continue; + } + + $matched[] = $path; + + $method = CG_WHOLE_FILE; + $statements = 0; + $covered = 0; + $local = []; + + foreach ($entry->line as $line) { + $type = (string) $line['type']; + + if ($type === 'method') { + $named = (string) $line['name']; + $method = ($named === '' ? CG_WHOLE_FILE : $named); + continue; + } + + if ($type !== 'stmt') { + continue; + } + + $key = ($path . '::' . $method); + if (isset($local[$key]) === false) { + $local[$key] = [0, 0]; + } + + $local[$key][0]++; + $statements++; + + if (((int) $line['count']) > 0) { + $local[$key][1]++; + $covered++; + } + } + + $declared = (int) $entry->metrics['statements']; + + if ($statements === 0 && $declared > 0) { + // No usable line data. Measuring 0/0 here would drop the file out of + // the comparison entirely, so fall back to the file metric and say so. + $fellBack[] = $path; + $local = [ + ($path . '::' . CG_WHOLE_FILE) => [$declared, (int) $entry->metrics['coveredstatements']], + ]; + echo " note: {$label} report carries no line data for {$path}; " + . "falling back to its file-level metric ({$entry->metrics['coveredstatements']}/{$declared}).\n"; + } else if ($statements !== $declared) { + echo " note: {$label} report declares {$declared} statement(s) for {$path} but emits " + . "{$statements} attributable line(s); the attributable ones are what is compared.\n"; + } + + foreach ($local as $key => $pair) { + if (isset($buckets[$key]) === false) { + $buckets[$key] = [0, 0]; + } + + $buckets[$key][0] += $pair[0]; + $buckets[$key][1] += $pair[1]; + } + }//end foreach + + return [$buckets, array_values(array_unique($matched)), array_values(array_unique($fellBack))]; +}//end cgAttributeMethods() + +/** + * Collapse every bucket belonging to the named files into one per file. + * + * THIS IS THE HOLE THAT WRITING THE TESTS FOUND, and it is worth naming because + * it is the invisible-pass shape in its purest form. If one side of the + * comparison cannot be attributed to methods it falls back to a single + * CG_WHOLE_FILE bucket — but that bucket's key then exists on ONE side only, so + * the asymmetric rule classifies the entire base-side file as "deleted", drops + * it, finds nothing left to compare, and reports: + * + * OK: none of the changed PHP existed at the merge base + * + * A file the guard could not read would have passed every possible drop. So when + * EITHER side falls back for a file, BOTH sides are collapsed to that file's + * single bucket: the key then exists on both sides, the file is compared exactly + * as the file-scoped mode compares it today, and the deletion penalty applies to + * it. Conservative, and visible in the output. + * + * @param array $buckets + * @param array $paths + * + * @return array + */ +function cgCollapseFiles(array $buckets, array $paths): array +{ + if (empty($paths) === true) { + return $buckets; + } + + $collapse = array_fill_keys($paths, true); + $out = []; + + foreach ($buckets as $key => $pair) { + $split = strrpos($key, '::'); + $path = ($split === false ? $key : substr($key, 0, $split)); + + if (isset($collapse[$path]) === true) { + $key = ($path . '::' . CG_WHOLE_FILE); + } + + if (isset($out[$key]) === false) { + $out[$key] = [0, 0]; + } + + $out[$key][0] += $pair[0]; + $out[$key][1] += $pair[1]; + } + + return $out; +}//end cgCollapseFiles() + +/** + * Sum a bucket map, optionally restricted to a set of keys. + * + * @param array $buckets + * @param array|null $keep Keys to include, or null for all. + * + * @return array{0:int,1:int} statements, covered + */ +function cgSumBuckets(array $buckets, ?array $keep = null): array +{ + $statements = 0; + $covered = 0; + + foreach ($buckets as $key => $pair) { + if ($keep !== null && isset($keep[$key]) === false) { + continue; + } + + $statements += $pair[0]; + $covered += $pair[1]; + } + + return [$statements, $covered]; +}//end cgSumBuckets() + /** * Read the changed-file list, keeping only PHP files the guard can measure. * @@ -299,6 +546,15 @@ function cgReport(string $label, int $statements, int $covered, float $percentag $baselineFile = (__DIR__ . '/../.coverage-baseline'); $against = ($options['against'] ?? null); $changedList = ($options['changed-files'] ?? null); +$deletionFree = isset($options['deletion-neutral']); + +// A flag that is accepted and ignored is the silent-downgrade shape this script's +// `--capabilities` probe exists to prevent, so refuse rather than fall through to +// a comparison the caller did not ask for. +if ($deletionFree === true && (is_string($changedList) === false || $changedList === '')) { + fwrite(STDERR, "Error: --deletion-neutral requires --changed-files (and therefore --against). It refines the file-scoped comparison; it has no meaning against the whole project.\n"); + exit(CG_INPUT); +} // ── scoped mode: compare ONLY the PHP the change touched ──────────────────── // @@ -321,6 +577,153 @@ function cgReport(string $label, int $statements, int $covered, float $percentag echo 'Scoped to ' . count($changed) . " changed PHP file(s).\n"; + // ── deletion-neutral mode ─────────────────────────────────────────────── + // + // WHAT THIS FIXES. `cgRatioDropped()` is a single integer cross-product with + // no tolerance, so the file-scoped ratchet demands, exactly: + // + // the code you touched must be at least as well covered as the code you + // did not. + // + // For ADDITIONS that is right, and it earns its keep: openconnector#1265 + // covered 27 of 54 new statements against a 61.86% floor, needed 34, and + // writing the missing seven is what exposed a cascade that deleted nothing + // and reported success. + // + // For DELETIONS it INVERTS. Removing `d` statements of which `c` were covered + // lowers the ratio whenever c/d exceeds the ratio of what remains — i.e. + // whenever the deleted code was better tested than average. And dead code is + // dead because nothing CALLS it, not because nothing TESTED it: gate-57 + // (orphaned-write-capability) exists to find precisely that code, so gate-57 + // and this ratchet are in arithmetic opposition, not tension. Such a pull + // request cannot satisfy the ratchet from inside its own subject at all — + // the only moves are delete less, add filler, or delete additional + // *uncovered* statements until it balances. The gate can be satisfied by + // deleting more code and cannot be satisfied by testing anything. + // + // Measured, both blocked by the file-scoped rule: + // opencatalogi#895 head 112/115 (97.39%) base 148/151 (98.01%) -0.62% + // launchpad#128 head 183/254 (72.05%) base 220/304 (72.37%) -0.32% + // + // THE RULE, AND IT IS ASYMMETRIC ON PURPOSE: + // + // drop BASE-ONLY methods (deletions) from the base side; + // KEEP HEAD-ONLY methods (additions) on the head side. + // + // The symmetric version — "compare over statements present in both reports" — + // is the obvious one and it is broken. It also drops head-only statements, so + // a change adding 40 new statements with 0 of them covered compares an empty + // set to an empty set and PASSES. That is the openconnector#1265 shape, and + // the symmetric rule would have retired the half of this ratchet that works. + // A mutant reintroducing it is in the test suite and must stay there. + // + // Consequences, all four measured in quality-config/tests/test-coverage-guard.py: + // pure deletion (opencatalogi) PASS 112/115 vs 112/115 — exactly neutral + // pure deletion (launchpad) PASS 183/254 vs 183/254 + // regression in survivors FAIL 180/254 vs 183/254 + // new untested code FAIL 183/294 (62.24%) vs 183/254 (72.05%) + // + // KNOWN RESIDUAL, stated rather than discovered: a RENAME reads as a delete + // plus an add. The old name leaves the base side, the new name arrives on the + // head side and must be covered. That is the right incentive — a renamed + // method is new code as far as the test suite is concerned — but it means a + // pure rename of a well-covered method is not free, and an author who did not + // expect it will read the failure as noise. It is documented here and in the + // failure message so it is legible when it happens. + if ($deletionFree === true) { + [$headBuckets, $headFiles, $headFellBack] = cgAttributeMethods($cloverFile, 'current', $changed); + [$baseBuckets, $baseFiles, $baseFellBack] = cgAttributeMethods($against, 'merge-base', $changed); + + // Either side falling back forces BOTH sides to file level for that file — + // see cgCollapseFiles() for why anything else is an invisible pass. + $collapse = array_values(array_unique(array_merge($headFellBack, $baseFellBack))); + if (empty($collapse) === false) { + echo 'Falling back to file-level metrics on both sides for: ' . implode(', ', $collapse) . "\n"; + $headBuckets = cgCollapseFiles($headBuckets, $collapse); + $baseBuckets = cgCollapseFiles($baseBuckets, $collapse); + } + + echo 'Deletion-neutral: attributed by method name (head ' . count($headFiles) . ' file(s), ' + . 'base ' . count($baseFiles) . ' file(s), ' . count($headBuckets) . ' head method(s), ' + . count($baseBuckets) . " base method(s)).\n"; + + // A changed file the base measured and the head did not is normally a + // DELETED file, and treating it as deleted is the point of this mode. But + // it is also what an accidental coverage exclusion looks like, and the two + // are indistinguishable from here — so it is said out loud rather than + // absorbed silently. + $goneFiles = array_values(array_diff($baseFiles, $headFiles)); + if (empty($goneFiles) === false) { + echo 'Measured at the merge base and absent from the head report: ' + . implode(', ', $goneFiles) . "\n"; + echo " Treated as deleted. If one of those files still exists, it has been dropped from\n"; + echo " coverage measurement and that is the thing to fix, not this guard.\n"; + } + + if (empty($headBuckets) === true && empty($baseBuckets) === true) { + echo "OK: none of the changed PHP files appear in either coverage report.\n"; + echo " Nothing was measured, so nothing is claimed about them.\n"; + exit(CG_OK); + } + + $keep = []; + foreach ($headBuckets as $key => $unused) { + $keep[$key] = true; + } + + $dropped = []; + foreach ($baseBuckets as $key => $pair) { + if (isset($keep[$key]) === false) { + $dropped[$key] = $pair; + } + } + + [$statements, $covered] = cgSumBuckets($headBuckets); + [$baseStatements, $baseCovered] = cgSumBuckets($baseBuckets, $keep); + + $current = ($statements > 0 ? round((($covered / $statements) * 100), 2) : 0.0); + $base = ($baseStatements > 0 ? round((($baseCovered / $baseStatements) * 100), 2) : 0.0); + + if (empty($dropped) === false) { + [$droppedStatements, $droppedCovered] = cgSumBuckets($dropped); + echo 'Removed from the base side: ' . count($dropped) + . " method(s) absent from head, {$droppedCovered}/{$droppedStatements} statements.\n"; + echo " A method that no longer exists is not a coverage regression; it is deleted code.\n"; + } + + cgReport('Surviving code, head:', $statements, $covered, $current); + cgReport('Surviving code, base:', $baseStatements, $baseCovered, $base); + + if ($baseStatements === 0) { + echo "OK: none of the changed PHP existed at the merge base, so there is no prior figure to drop below.\n"; + exit(CG_OK); + } + + if (cgRatioDropped($covered, $statements, $baseCovered, $baseStatements) === true) { + $delta = round(($base - $current), 2); + echo($delta > 0 + ? "FAIL: coverage of the code this change KEEPS or ADDS dropped by {$delta}%.\n" + : "FAIL: coverage of the code this change KEEPS or ADDS dropped by less than 0.01% — too " + . "little to show in the percentage, but a real loss in the counts below.\n"); + echo " base {$baseCovered}/{$baseStatements} -> head {$covered}/{$statements} statements, " + . "comparing only methods that exist on BOTH sides plus everything new on this branch.\n"; + + if ($statements > $baseStatements) { + $added = ($statements - $baseStatements); + echo " This change adds {$added} statements to those files. Adding code without tests drops coverage.\n"; + } + + echo " Deleted methods were already excluded, so this is not a deletion penalty. If you\n"; + echo " RENAMED a method, note that a rename reads as a delete plus an add: the new name is\n"; + echo " new code here and has to be covered.\n"; + + exit(CG_DROPPED); + } + + echo "OK: coverage of the surviving and added code did not drop.\n"; + exit(CG_OK); + }//end if + [$statements, $covered, $current] = cgMeasureFiles($cloverFile, 'current', $changed); [$baseStatements, $baseCovered, $base] = cgMeasureFiles($against, 'merge-base', $changed); diff --git a/src/App.vue b/src/App.vue index cb763aa48..7a9be41ea 100644 --- a/src/App.vue +++ b/src/App.vue @@ -99,6 +99,13 @@ appId="keepiq" :translate="translateForApp" :permissions="permissions"> + + + + + + + diff --git a/src/components/KeepiqAppNav/NavFolderTree.vue b/src/components/KeepiqAppNav/NavFolderTree.vue new file mode 100644 index 000000000..c2e87ecbf --- /dev/null +++ b/src/components/KeepiqAppNav/NavFolderTree.vue @@ -0,0 +1,172 @@ + + + + + + diff --git a/src/components/SecretListItem.vue b/src/components/SecretListItem.vue index 42c7c02a1..4d02e4dc4 100644 --- a/src/components/SecretListItem.vue +++ b/src/components/SecretListItem.vue @@ -92,9 +92,13 @@ diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts index e9d79913a..12798895e 100644 --- a/tests/e2e/global-setup.ts +++ b/tests/e2e/global-setup.ts @@ -129,6 +129,39 @@ export default async function globalSetup(config: FullConfig): Promise { ) } + /* + * Suppress the product walkthrough (ADR-043) for automated runs, the way + * dossiq's global-setup already does. The tour's "seen" state is per USER, + * not per test, so whichever spec runs first wears the tour and the rest + * inherit a dismissed one — an order-dependent suite. `audit-trail.spec.ts` + * started needing a retry the moment the walkthrough landed (#484), having + * never retried in the three preceding runs on development. + * + * This is NOT covered by #486: that withholds the tour while the vault is + * LOCKED, which is a different concern. Every spec here unlocks, and the + * tour is offered on the first unlocked visit by design. + * + * The sentinel is deliberately higher than any real app version, so every + * step's `sinceVersion` sorts below it and the tour composes to an empty + * step set rather than merely starting dismissed. localStorage is the right + * lever despite keepiq declaring a `completionConfigKey`: + * loadWalkthroughSeenVersion() reads a `{ value: null }` server answer — + * what a fresh user gets — as "never seen" and returns the local mirror, so + * the seeded value wins. The page is already on the instance origin after + * login, which is the origin storageState persists. + */ + try { + await page.evaluate(() => { + try { + window.localStorage.setItem('cn-walkthrough-seen:keepiq', '999.0.0') + } catch (e) { + // localStorage unavailable — specs fall back to dismissing by hand. + } + }) + } catch { + // Never fail setup over an optional convenience. + } + await context.storageState({ path: STORAGE_STATE }) await browser.close() } diff --git a/tests/e2e/spec-coverage/navigation.spec.ts b/tests/e2e/spec-coverage/navigation.spec.ts index 9f7dbf74d..b48612409 100644 --- a/tests/e2e/spec-coverage/navigation.spec.ts +++ b/tests/e2e/spec-coverage/navigation.spec.ts @@ -9,7 +9,7 @@ * that clicking a gated nav entry routes through the lock guard. Nav clicks are * scoped to the app navigation (never the global NC header) per the nav trap. * - * @e2e openspec/specs/dashboard/spec.md#app-navigation-renders + * @e2e openspec/specs/menu-architecture/spec.md#app-navigation-renders */ import { test, expect } from '@playwright/test' import { diff --git a/tests/e2e/workflows/_workflow-helpers.ts b/tests/e2e/workflows/_workflow-helpers.ts index 1275d640c..faa6ae841 100644 --- a/tests/e2e/workflows/_workflow-helpers.ts +++ b/tests/e2e/workflows/_workflow-helpers.ts @@ -192,6 +192,34 @@ export async function openVault(page: Page): Promise { await expect(page.locator('.secret-list-view')).toBeVisible({ timeout: 20_000 }) } +/** + * Click an entry inside the vault toolbar's "More actions" overflow. + * + * The overflow entries are NcActionButtons (restyle Stage 5), and NcActionButton + * renders its `data-testid` on the `
  • ` WRAPPER while the + * Vue click handler sits on the inner `button.action-button`. A native click on + * the element that carries the testid therefore hits the presentational list + * item and fires nothing — the dialog never opens and the spec times out on its + * next locator. So: open the menu, then natively click the INNER button of the + * testid-carrying wrapper (native, because the themed button swallows + * Playwright's synthetic click, same as the unlock button above). + * + * @param page The Playwright page (must be on the vault list, unlocked). + * @param testid The `data-testid` of the overflow entry, e.g. 'import-secrets'. + */ +export async function clickOverflowAction( + page: Page, + testid: string, +): Promise { + await page + .getByRole('button', { name: /More actions/i }) + .first() + .evaluate((el: HTMLElement) => el.click()) + const inner = page.getByTestId(testid).locator('button').first() + await inner.waitFor({ state: 'attached', timeout: 10_000 }) + await inner.evaluate((el: HTMLElement) => el.click()) +} + /** * Navigate to an in-app route WITHIN the already-unlocked SPA, in place. * diff --git a/tests/e2e/workflows/compromise-recovery.spec.ts b/tests/e2e/workflows/compromise-recovery.spec.ts index 6a78517a2..3478ee946 100644 --- a/tests/e2e/workflows/compromise-recovery.spec.ts +++ b/tests/e2e/workflows/compromise-recovery.spec.ts @@ -378,8 +378,18 @@ test.describe('Workflow: compromise recovery — encryption-suites/spec.md', () // row ever said anything. // The user-settings dialog is modal. Navigating the hash underneath it // leaves the vault list mounted but covered, so the row indicator is - // never shown — close the dialog first. - await page.keyboard.press('Escape') + // never shown — close the dialogs first. They can be STACKED (the + // recovery dialog sits on the user-settings dialog) and Escape closes + // only the topmost, so press it once per remaining dialog instead of + // once in total — the single-press form left one dialog standing and + // was the recorded flake on CI. + for (let i = 0; i < 5; i++) { + if ((await page.locator('[role="dialog"]').count()) === 0) { + break + } + await page.keyboard.press('Escape') + await page.waitForTimeout(500) + } await expect(page.locator('[role="dialog"]')).toHaveCount(0, { timeout: 15_000, }) diff --git a/tests/e2e/workflows/export-gdpr.spec.ts b/tests/e2e/workflows/export-gdpr.spec.ts index 495b07c3f..56fea5d40 100644 --- a/tests/e2e/workflows/export-gdpr.spec.ts +++ b/tests/e2e/workflows/export-gdpr.spec.ts @@ -38,11 +38,11 @@ test.describe('secret export + GDPR', () => { await unlockVault(page, DEV_MASTER_PASSWORD) await gotoVaultRoute(page, 'secrets') - // Open the "My data" actions and start an export. + // Open the "More actions" overflow and start an export. // The themed NcButton swallows Playwright's synthetic click; fire a native - // click so the "My data" actions menu opens. + // click so the "More actions" overflow menu opens. await page - .getByRole('button', { name: /My data/i }) + .getByRole('button', { name: /More actions/i }) .evaluate((el: HTMLElement) => el.click()) await page .getByRole('menuitem', { name: /Export data/i }) @@ -69,9 +69,9 @@ test.describe('secret export + GDPR', () => { await gotoVaultRoute(page, 'secrets') // The themed NcButton swallows Playwright's synthetic click; fire a native - // click so the "My data" actions menu opens. + // click so the "More actions" overflow menu opens. await page - .getByRole('button', { name: /My data/i }) + .getByRole('button', { name: /More actions/i }) .evaluate((el: HTMLElement) => el.click()) await page .getByRole('menuitem', { name: /Export data/i }) @@ -102,9 +102,9 @@ test.describe('secret export + GDPR', () => { await gotoVaultRoute(page, 'secrets') // The themed NcButton swallows Playwright's synthetic click; fire a native - // click so the "My data" actions menu opens. + // click so the "More actions" overflow menu opens. await page - .getByRole('button', { name: /My data/i }) + .getByRole('button', { name: /More actions/i }) .evaluate((el: HTMLElement) => el.click()) await page .getByRole('menuitem', { name: /Delete my Keepiq data/i }) @@ -129,9 +129,9 @@ test.describe('secret export + GDPR', () => { await gotoVaultRoute(page, 'secrets') // The themed NcButton swallows Playwright's synthetic click; fire a native - // click so the "My data" actions menu opens. + // click so the "More actions" overflow menu opens. await page - .getByRole('button', { name: /My data/i }) + .getByRole('button', { name: /More actions/i }) .evaluate((el: HTMLElement) => el.click()) await page .getByRole('menuitem', { name: /GDPR export/i }) diff --git a/tests/e2e/workflows/folder-sharing.spec.ts b/tests/e2e/workflows/folder-sharing.spec.ts index c6ed78130..bf42b86b1 100644 --- a/tests/e2e/workflows/folder-sharing.spec.ts +++ b/tests/e2e/workflows/folder-sharing.spec.ts @@ -39,7 +39,12 @@ * `secrets-write-ui` scenarios they actually drive. */ import { test, expect } from '@playwright/test' -import { gotoLockSettled, unlockVault, openVault } from './_workflow-helpers' +import { + clickOverflowAction, + gotoLockSettled, + openVault, + unlockVault, +} from './_workflow-helpers' const REQ_TOKEN = `(() => { const head = document.querySelector('head[data-requesttoken]'); @@ -161,22 +166,30 @@ test.describe('Workflow: folders + sharing — folders/spec.md', () => { await unlockVault(page) await openVault(page) - await nativeClickByText( - page, - '.secret-list-view__sidebar button', - 'New folder', - ) + // The create affordance lives in the toolbar's "More actions" overflow + // (restyle Stage 5); at the vault root its label is "New vault". The + // helper clicks the NcActionButton's INNER button — the testid sits on + // the presentational
  • , whose click fires nothing. + await clickOverflowAction(page, 'open-create-folder') await expect(page.locator('.folder-form')).toBeVisible({ timeout: 10_000 }) await page .locator('.folder-form input[type="text"]') .first() .fill(FOLDER, { force: true }) await page.waitForTimeout(300) - await nativeClickByText(page, 'body button', 'Create folder') + // At the vault root the dialog's submit reads "Create vault" + // (level-appropriate terminology). + await nativeClickByText(page, 'body button', 'Create (vault|folder)') await expect(page.locator('.folder-form')).toHaveCount(0, { timeout: 15_000, }) + // The new vault appears in the app nav's folder tree (restyle Stage 7) + // — the create dialog's onSaved refetches the shared folder store. + await expect( + page.locator('[data-testid="nav-folder-tree"]').getByText(FOLDER), + ).toBeVisible({ timeout: 15_000 }) + const folder = await page.evaluate( async ({ tokExpr, name }) => { // eslint-disable-next-line no-eval @@ -195,9 +208,11 @@ test.describe('Workflow: folders + sharing — folders/spec.md', () => { ) expect(folder, 'folder must be persisted').toBeTruthy() - await expect( - page.locator('.secret-list-view__sidebar', { hasText: FOLDER }), - ).toBeVisible({ timeout: 10_000 }) + // The in-page folder pane is gone (restyle Stage 6): at the vault root + // the new vault appears as a SUBFOLDER ROW in the list itself. + await expect(page.getByTestId(`folder-row-${folder.id}`)).toBeVisible({ + timeout: 10_000, + }) await page.evaluate( async ({ tokExpr, id }) => { @@ -499,4 +514,82 @@ test.describe('Workflow: folders + sharing — folders/spec.md', () => { 'the deferred user-share affordance issued a request', ).toEqual([]) }) + + /* + * Deep folder navigation purely through the LIST (restyle Stage 6): the + * vault root shows top-level vaults as rows, clicking one descends, the + * breadcrumb trail appears inside a folder, and clicking a crumb walks + * back up — no sidebar involvement. + */ + test('navigates root → vault → nested folder via subfolder rows and breadcrumbs', async ({ + page, + }) => { + // @e2e secrets-write-ui::create-a-folder + const PARENT = `__e2e_nav_vault_${Date.now()}` + const CHILD = `__e2e_nav_sub_${Date.now()}` + await unlockVault(page) + await openVault(page) + + // Seed a vault + nested folder via the API (the create dialog is + // covered by its own test above; this one is about navigation). + const ids = await page.evaluate( + async ({ tokExpr, parent, child }) => { + // eslint-disable-next-line no-eval + const token = eval(tokExpr) + const mk = async (name: string, parentId: string | null) => { + const res = await fetch( + '/index.php/apps/keepiq/api/v1/folders', + { + method: 'POST', + credentials: 'include', + headers: { + requesttoken: token, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name, parentId }), + }, + ) + return (await res.json()).id as string + } + const parentId = await mk(parent, null) + const childId = await mk(child, parentId) + return { parentId, childId } + }, + { tokExpr: REQ_TOKEN, parent: PARENT, child: CHILD }, + ) + + // The freshly created folders only exist server-side, and the folder + // store refetches on unlock. A reload alone cannot surface them: it + // wipes the in-memory CryptoKey, so the app lands back on the lock + // gate and no vault row ever renders. Reload, unlock again, reopen + // the vault (domcontentloaded, not networkidle — ADR-074 rule 4). + await page.reload({ waitUntil: 'domcontentloaded' }) + await unlockVault(page) + await openVault(page) + + // Root: the new vault appears as a subfolder row; descend into it. + const parentRow = page.getByTestId(`folder-row-${ids.parentId}`) + await expect(parentRow).toBeVisible({ timeout: 20_000 }) + await parentRow.evaluate((el: HTMLElement) => el.click()) + + // Inside the vault: breadcrumbs render, the nested folder is a row. + await expect(page.getByTestId('cn-breadcrumbs')).toBeVisible({ + timeout: 20_000, + }) + const childRow = page.getByTestId(`folder-row-${ids.childId}`) + await expect(childRow).toBeVisible({ timeout: 20_000 }) + await childRow.evaluate((el: HTMLElement) => el.click()) + + // Inside the nested folder: the trail carries the parent as a LINK; + // clicking it walks back up to the vault. + const crumbs = page.getByTestId('cn-breadcrumbs') + await expect(crumbs).toContainText(CHILD, { timeout: 20_000 }) + await crumbs + .getByText(PARENT) + .first() + .evaluate((el: HTMLElement) => el.click()) + await expect(page.getByTestId(`folder-row-${ids.childId}`)).toBeVisible({ + timeout: 20_000, + }) + }) }) diff --git a/tests/e2e/workflows/secret-import.spec.ts b/tests/e2e/workflows/secret-import.spec.ts index 2a528643d..72215260f 100644 --- a/tests/e2e/workflows/secret-import.spec.ts +++ b/tests/e2e/workflows/secret-import.spec.ts @@ -24,6 +24,7 @@ */ import { test, expect } from '@playwright/test' import { + clickOverflowAction, DEV_MASTER_PASSWORD, gotoLockSettled, gotoVaultRoute, @@ -44,11 +45,10 @@ test.describe('secret import', () => { await unlockVault(page, DEV_MASTER_PASSWORD) await gotoVaultRoute(page, 'secrets') - // The themed NcButton swallows Playwright's synthetic click, so fire a - // native HTMLButtonElement.click() to open the import wizard. - await page - .getByTestId('import-secrets') - .evaluate((el: HTMLElement) => el.click()) + // Import lives in the "More actions" overflow (restyle Stage 5). The + // helper clicks the NcActionButton's INNER button — the testid sits on + // the presentational
  • , whose click fires nothing. + await clickOverflowAction(page, 'import-secrets') // Upload the CSV via the file input (read entirely client-side). await page.getByTestId('import-file').setInputFiles({ @@ -85,11 +85,10 @@ test.describe('secret import', () => { await unlockVault(page, DEV_MASTER_PASSWORD) await gotoVaultRoute(page, 'secrets') - // The themed NcButton swallows Playwright's synthetic click, so fire a - // native HTMLButtonElement.click() to open the import wizard. - await page - .getByTestId('import-secrets') - .evaluate((el: HTMLElement) => el.click()) + // Import lives in the "More actions" overflow (restyle Stage 5). The + // helper clicks the NcActionButton's INNER button — the testid sits on + // the presentational
  • , whose click fires nothing. + await clickOverflowAction(page, 'import-secrets') await page.getByTestId('import-file').setInputFiles({ name: 'sample.csv', mimeType: 'text/csv', diff --git a/tests/e2e/workflows/vault-unlock.spec.ts b/tests/e2e/workflows/vault-unlock.spec.ts index 6116d0810..9f4399509 100644 --- a/tests/e2e/workflows/vault-unlock.spec.ts +++ b/tests/e2e/workflows/vault-unlock.spec.ts @@ -164,6 +164,15 @@ test.describe('Workflow: vault unlock — encryption-suites/spec.md', () => { // `:support-dialog="false"` would silence it by removing a feature // rather than by judging the traffic. /\/api\/preferences\/support-dialog-seen\b/, + // NOTE: `walkthrough_completed_version` is deliberately NOT listed. + // It was added here in #488 when the walkthrough merge put that + // probe on the wire behind the lock screen — but #486 had already + // fixed the cause the better way, by withholding `walkthrough` from + // the manifest while locked (`manifestForLockState`), so + // CnAppRoot never resolves the preference at all. The two landed + // minutes apart and the allowlist entry became an entry that CANNOT + // FIRE, which the note above names as pure masking surface: it would + // silently swallow a real regression of exactly this shape. ] page.on('request', (req) => { @@ -288,7 +297,10 @@ test.describe('Workflow: vault unlock — encryption-suites/spec.md', () => { * would break the other specs). The setup form's structure + 12-char strength * gating is already covered by the spec-coverage lock-screen spec. */ - test.fixme('first-time setup creates a suite and unlocks (needs a suite-less account)', async () => { - // Intentionally empty — see block comment for the precise blocker. + test('first-time setup creates a suite and unlocks', async () => { + test.fixme( + true, + "SETUP MODE is not drivable: an active suite already exists for admin, so the first-time setup form never renders. Driving it needs a suite-less account — there is no UI to revoke or delete the admin suite, and doing it out-of-band would break the other specs in this file. The setup form's structure and its 12-character strength gating are already covered by the spec-coverage lock-screen spec.", + ) }) }) diff --git a/tests/l10n/parity-ratchet.json b/tests/l10n/parity-ratchet.json index d3500778e..63779f30a 100644 --- a/tests/l10n/parity-ratchet.json +++ b/tests/l10n/parity-ratchet.json @@ -1,74 +1,74 @@ { - "backend (.json) be": 572, - "backend (.json) bg": 572, - "backend (.json) bs": 572, - "backend (.json) ca": 572, - "backend (.json) cs": 572, - "backend (.json) da": 572, - "backend (.json) de": 572, - "backend (.json) el": 572, - "backend (.json) es": 572, - "backend (.json) et": 572, - "backend (.json) fi": 572, - "backend (.json) fr": 572, - "backend (.json) ga": 572, - "backend (.json) hr": 572, - "backend (.json) hu": 572, - "backend (.json) is": 572, - "backend (.json) it": 572, - "backend (.json) lb": 572, - "backend (.json) lt": 572, - "backend (.json) lv": 572, - "backend (.json) mk": 572, - "backend (.json) mt": 572, - "backend (.json) nb": 572, - "backend (.json) nl": 413, - "backend (.json) pl": 572, - "backend (.json) pt": 572, - "backend (.json) rm": 572, - "backend (.json) ro": 572, - "backend (.json) ru": 572, - "backend (.json) sk": 572, - "backend (.json) sl": 572, - "backend (.json) sq": 572, - "backend (.json) sr": 572, - "backend (.json) sv": 572, - "backend (.json) tr": 572, - "backend (.json) uk": 572, - "frontend (.js) be": 572, - "frontend (.js) bg": 572, - "frontend (.js) bs": 572, - "frontend (.js) ca": 572, - "frontend (.js) cs": 572, - "frontend (.js) da": 572, - "frontend (.js) de": 572, - "frontend (.js) el": 572, - "frontend (.js) es": 572, - "frontend (.js) et": 572, - "frontend (.js) fi": 572, - "frontend (.js) fr": 572, - "frontend (.js) ga": 572, - "frontend (.js) hr": 572, - "frontend (.js) hu": 572, - "frontend (.js) is": 572, - "frontend (.js) it": 572, - "frontend (.js) lb": 572, - "frontend (.js) lt": 572, - "frontend (.js) lv": 572, - "frontend (.js) mk": 572, - "frontend (.js) mt": 572, - "frontend (.js) nb": 572, - "frontend (.js) nl": 413, - "frontend (.js) pl": 572, - "frontend (.js) pt": 572, - "frontend (.js) rm": 572, - "frontend (.js) ro": 572, - "frontend (.js) ru": 572, - "frontend (.js) sk": 572, - "frontend (.js) sl": 572, - "frontend (.js) sq": 572, - "frontend (.js) sr": 572, - "frontend (.js) sv": 572, - "frontend (.js) tr": 572, - "frontend (.js) uk": 572 + "backend (.json) be": 568, + "backend (.json) bg": 568, + "backend (.json) bs": 568, + "backend (.json) ca": 568, + "backend (.json) cs": 568, + "backend (.json) da": 568, + "backend (.json) de": 568, + "backend (.json) el": 568, + "backend (.json) es": 568, + "backend (.json) et": 568, + "backend (.json) fi": 568, + "backend (.json) fr": 568, + "backend (.json) ga": 568, + "backend (.json) hr": 568, + "backend (.json) hu": 568, + "backend (.json) is": 568, + "backend (.json) it": 568, + "backend (.json) lb": 568, + "backend (.json) lt": 568, + "backend (.json) lv": 568, + "backend (.json) mk": 568, + "backend (.json) mt": 568, + "backend (.json) nb": 568, + "backend (.json) nl": 409, + "backend (.json) pl": 568, + "backend (.json) pt": 568, + "backend (.json) rm": 568, + "backend (.json) ro": 568, + "backend (.json) ru": 568, + "backend (.json) sk": 568, + "backend (.json) sl": 568, + "backend (.json) sq": 568, + "backend (.json) sr": 568, + "backend (.json) sv": 568, + "backend (.json) tr": 568, + "backend (.json) uk": 568, + "frontend (.js) be": 568, + "frontend (.js) bg": 568, + "frontend (.js) bs": 568, + "frontend (.js) ca": 568, + "frontend (.js) cs": 568, + "frontend (.js) da": 568, + "frontend (.js) de": 568, + "frontend (.js) el": 568, + "frontend (.js) es": 568, + "frontend (.js) et": 568, + "frontend (.js) fi": 568, + "frontend (.js) fr": 568, + "frontend (.js) ga": 568, + "frontend (.js) hr": 568, + "frontend (.js) hu": 568, + "frontend (.js) is": 568, + "frontend (.js) it": 568, + "frontend (.js) lb": 568, + "frontend (.js) lt": 568, + "frontend (.js) lv": 568, + "frontend (.js) mk": 568, + "frontend (.js) mt": 568, + "frontend (.js) nb": 568, + "frontend (.js) nl": 409, + "frontend (.js) pl": 568, + "frontend (.js) pt": 568, + "frontend (.js) rm": 568, + "frontend (.js) ro": 568, + "frontend (.js) ru": 568, + "frontend (.js) sk": 568, + "frontend (.js) sl": 568, + "frontend (.js) sq": 568, + "frontend (.js) sr": 568, + "frontend (.js) sv": 568, + "frontend (.js) tr": 568, + "frontend (.js) uk": 568 } diff --git a/tests/router/guards.spec.js b/tests/router/guards.spec.js index 65cfe5f3a..3cfa78471 100644 --- a/tests/router/guards.spec.js +++ b/tests/router/guards.spec.js @@ -12,16 +12,17 @@ * @spec openspec/specs/encryption-suites/spec.md#requirement-session-mechanism */ -import { describe, it, expect, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' +import manifest from '../../src/manifest.json' import { - LOCK_ROUTE_NAME, - PUBLIC_ROUTE_NAMES, createVaultGuard, handleLockTransition, isPublicRoute, isPublicSurface, + LOCK_ROUTE_NAME, + manifestForLockState, + PUBLIC_ROUTE_NAMES, } from '../../src/router/guards.js' -import manifest from '../../src/manifest.json' /** * Build a guard plus a spy `next`, over a session store of the given state. @@ -416,3 +417,55 @@ describe('isPublicSurface', () => { ).toBe(true) }) }) + +describe('manifestForLockState', () => { + const MANIFEST = { + version: 1, + pages: [], + walkthrough: { enabled: true, tours: [] }, + } + + it('withholds the walkthrough while the vault is locked', () => { + // CnAppRoot fetches the tour's completion preference as soon as it sees + // `manifest.walkthrough`, and it mounts OUTSIDE the router guard — so + // this is the only thing keeping that request off the wire behind the + // lock screen. + const shown = manifestForLockState(MANIFEST, { isLocked: true }) + + expect(shown.walkthrough).toBeUndefined() + expect(shown.pages).toBe(MANIFEST.pages) + }) + + it('offers the walkthrough once the vault is unlocked', () => { + // Withheld, not disabled: a first-visit tour must still run on a user's + // first UNLOCKED visit. + expect(manifestForLockState(MANIFEST, { isLocked: false })).toBe(MANIFEST) + }) + + it('fails closed when the store is missing or its flag is not a boolean', () => { + // Same posture as createVaultGuard: only an explicit `false` unlocks. + // A store that failed to initialise must withhold the tour, not ship it. + for (const store of [ + undefined, + null, + {}, + { isLocked: undefined }, + { isLocked: 'no' }, + { isLocked: 0 }, + ]) { + expect(manifestForLockState(MANIFEST, store).walkthrough).toBeUndefined() + } + }) + + it('leaves a manifest without a walkthrough untouched', () => { + const plain = { version: 1, pages: [] } + + expect(manifestForLockState(plain, { isLocked: true })).toBe(plain) + }) + + it('does not mutate the manifest it was given', () => { + manifestForLockState(MANIFEST, { isLocked: true }) + + expect(MANIFEST.walkthrough).toBeDefined() + }) +}) diff --git a/tests/vitest/stubs/conduction-nextcloud-vue.js b/tests/vitest/stubs/conduction-nextcloud-vue.js index de304c213..5de9a1e5a 100644 --- a/tests/vitest/stubs/conduction-nextcloud-vue.js +++ b/tests/vitest/stubs/conduction-nextcloud-vue.js @@ -7,7 +7,7 @@ * The published library ships a fat bundle (its own @nextcloud/vue copy, * apexcharts, leaflet, codemirror, …) that neither resolves nor renders under * vitest's jsdom env. Views under test import only a handful of primitives - * (CnIndexPage, CnFolderSidebar, CnStatusBadge) and assert on emitted events + + * (CnIndexPage, CnBreadcrumbs, CnStatusBadge) and assert on emitted events + * the slots they pass down — not on the library's real markup. So we swap the * whole package for minimal Vue 3 render-function stubs. * @@ -106,50 +106,8 @@ const CnIndexPage = { }, } -/** - * CnFolderSidebar stub — an "All" button (emits `select` null) and an optional - * New-folder button (emits `create`). - */ -const CnFolderSidebar = { - name: 'CnFolderSidebar', - props: [ - 'folders', - 'selectedId', - 'allLabel', - 'allowCreate', - 'createLabel', - 'source', - 'objects', - 'groupBy', - ], - emits: ['select', 'create'], - render() { - const children = [ - h( - 'button', - { - class: 'cn-folder-sidebar__all', - onClick: () => this.$emit('select', null), - }, - this.allLabel, - ), - ] - if (this.allowCreate) { - children.push( - h( - 'button', - { - class: 'cn-folder-sidebar__new', - onClick: () => - this.$emit('create', { parentId: this.selectedId }), - }, - this.createLabel, - ), - ) - } - return h('div', { class: 'cn-folder-sidebar' }, children) - }, -} +// CnFolderSidebar stub removed with the in-page folder pane (restyle +// Stage 7) — folder navigation lives in KeepiqAppNav's tree now. /** CnStatusBadge stub — a pill span carrying the label. */ const CnStatusBadge = { @@ -164,4 +122,33 @@ const CnStatusBadge = { }, } -export { CnIndexPage, CnFolderSidebar, CnStatusBadge } +/** + * CnBreadcrumbs stub — one span per crumb (label or icon name), the last + * carrying aria-current="page", mirroring the real component's contract so + * tests can assert on the rendered trail. + */ +const CnBreadcrumbs = { + name: 'CnBreadcrumbs', + props: ['crumbs', 'ariaLabel'], + render() { + const crumbs = this.crumbs || [] + if (crumbs.length === 0) return null + return h( + 'nav', + { 'data-testid': 'cn-breadcrumbs', 'aria-label': this.ariaLabel }, + crumbs.map((crumb, index) => + h( + 'span', + { + class: 'cn-breadcrumbs__crumb', + 'aria-current': + index === crumbs.length - 1 ? 'page' : undefined, + }, + crumb.label || crumb.icon, + ), + ), + ) + }, +} + +export { CnIndexPage, CnStatusBadge, CnBreadcrumbs } diff --git a/tests/vitest/vault-list.spec.js b/tests/vitest/vault-list.spec.js new file mode 100644 index 000000000..40f964e6c --- /dev/null +++ b/tests/vitest/vault-list.spec.js @@ -0,0 +1,78 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction / Keepiq Contributors + * SPDX-License-Identifier: EUPL-1.2 + * + * Tests for the vault-list helpers (restyle Stage 6): the subfolder pseudo- + * rows the secret list shows above the secrets, and the "A / B / C" path + * labels the move dialogs use to disambiguate nested folders. + * + * @spec openspec/specs/secrets/spec.md#requirement-folder-management + */ + +import { describe, expect, it } from 'vitest' +import { folderPathLabel, subfolderRows } from '../../src/utils/vaultList.js' + +const FOLDERS = [ + { id: 'a', name: 'Alpha vault', parentId: null }, + { id: 'b', name: 'Beta vault', parentId: null }, + { id: 'a1', name: 'Nested', parentId: 'a' }, + { id: 'a2', name: 'Another nested', parentId: 'a' }, + { id: 'a1x', name: 'Deep', parentId: 'a1' }, +] + +describe('subfolderRows', () => { + it('lists the top-level vaults at the root, name-sorted as one group', () => { + const rows = subfolderRows(FOLDERS, null) + expect(rows.map((r) => r.name)).toEqual(['Alpha vault', 'Beta vault']) + }) + + it("lists only the selected folder's DIRECT children", () => { + const rows = subfolderRows(FOLDERS, 'a') + expect(rows.map((r) => r.folderId)).toEqual(['a2', 'a1']) + }) + + it('shapes pseudo-rows so they can never collide with secret ids', () => { + const [row] = subfolderRows(FOLDERS, 'a1') + expect(row).toEqual({ + id: 'folder:a1x', + folderId: 'a1x', + name: 'Deep', + isFolder: true, + }) + }) + + it('filters by the inline search term, case-insensitively', () => { + expect(subfolderRows(FOLDERS, 'a', 'NEST').map((r) => r.folderId)).toEqual([ + 'a2', + 'a1', + ]) + expect( + subfolderRows(FOLDERS, 'a', 'another').map((r) => r.folderId), + ).toEqual(['a2']) + expect(subfolderRows(FOLDERS, 'a', 'no-match')).toEqual([]) + }) + + it('is safe on empty and missing input', () => { + expect(subfolderRows([], null)).toEqual([]) + expect(subfolderRows(undefined, 'a')).toEqual([]) + }) +}) + +describe('folderPathLabel', () => { + it('renders the root-first path of a nested folder', () => { + expect(folderPathLabel(FOLDERS, 'a1x')).toBe('Alpha vault / Nested / Deep') + expect(folderPathLabel(FOLDERS, 'b')).toBe('Beta vault') + }) + + it('yields the empty string for an unknown id', () => { + expect(folderPathLabel(FOLDERS, 'nope')).toBe('') + }) + + it('terminates on a corrupt parentId cycle', () => { + const cyclic = [ + { id: 'x', name: 'X', parentId: 'y' }, + { id: 'y', name: 'Y', parentId: 'x' }, + ] + expect(folderPathLabel(cyclic, 'x')).toBe('Y / X') + }) +})