mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-04 13:33:22 +00:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55e17498a2 | |||
| a459d45c87 | |||
| b22d3cf33c | |||
| f7db586c7e | |||
| 23b7dc0d11 | |||
| daec0a877e | |||
| 57540164fa | |||
| 457fed0c86 | |||
| 4f38c9adee | |||
| f7d6fe929e | |||
| 1030f719fc | |||
| fbd6c3603d | |||
| 5bfc5a0883 | |||
| 94e4aa36e4 | |||
| 027e8532d2 | |||
| 2a82335e07 | |||
| 1aaab1bf66 | |||
| 503676647f | |||
| 5a7c9938dd | |||
| d1d1999216 | |||
| 51eea30dda | |||
| c18f2f0f73 | |||
| 3d6089a501 | |||
| 8f12c83d21 | |||
| 11e6876826 | |||
| 88c182cf3b | |||
| 30f66cff2b | |||
| cca3d9c314 | |||
| 7d1903f9db | |||
| 6b76c11192 | |||
| 283c79bcd6 | |||
| 1b6513f0fb | |||
| cb67a15e81 | |||
| 1c62036ed2 | |||
| 5e23474e8b | |||
| 202a1d7ad5 | |||
| 8b5d480670 | |||
| e279e83fd0 | |||
| 318550f866 | |||
| 9c991f495d | |||
| e9dabc39e3 | |||
| 9dcfe1eb26 | |||
| c210187dab | |||
| 23aacc2b2a | |||
| 469ba8d8c8 | |||
| be44b4b6aa | |||
| 7c6c49e5f4 | |||
| 31f9b22b53 | |||
| dd779fafbe | |||
| 18917adaf2 | |||
| 8156664ccc | |||
| ae510e3e34 | |||
| 352b19a061 | |||
| d1acf18d67 | |||
| 2567d9f193 | |||
| 7093d1e24b | |||
| d32562e75c | |||
| 24e3019e21 | |||
| 98597d462d | |||
| 6a4e6df21a | |||
| bba0d78499 | |||
| b33f4c389a | |||
| cab9291d29 | |||
| b671d0d74a | |||
| 44f015c066 | |||
| e87ceb60ba | |||
| d888145b21 | |||
| 997debc6a3 | |||
| 9192779ee4 | |||
| a8bc2156cd | |||
| 928173e8be | |||
| c052175690 | |||
| de615cbd79 | |||
| 3f36630d46 | |||
| a8a7f3f340 | |||
| 29f1eaaa1b | |||
| 57c4b892ce | |||
| 090e975bb3 | |||
| b72d30d89e | |||
| 046567fdfc | |||
| 4ce6793da9 | |||
| abe97a1f4d | |||
| 55b02d510d | |||
| 120ba10972 | |||
| e3c58818e2 | |||
| 6ddfc1aa5d | |||
| 463f2aac30 | |||
| 982a682803 | |||
| f446398dfd | |||
| 745f24c7d9 | |||
| dad8d21dc8 | |||
| 0b994aebe2 | |||
| d30c2ddbb1 | |||
| 48bc0d4960 | |||
| ed0d36de25 | |||
| 72749a9070 | |||
| 5cc4c6254b | |||
| 14333d08f1 | |||
| 8e5e73a2cd | |||
| 4f10401c36 | |||
| 0e3f7d5c68 | |||
| d53bb9a2f7 | |||
| 9b265e3b87 | |||
| 377dff7603 |
+9
-1
@@ -61,6 +61,15 @@ SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# Set this only if your web server and PHP-FPM run as different system
|
||||
# users — common on cPanel/Plesk shared hosting. Uploaded files are
|
||||
# written 0600 in 0700 directories, which nginx cannot read, and since
|
||||
# nginx is what actually streams a download (PHP authorizes, then hands
|
||||
# it the path) every download fails while the rest of the site works.
|
||||
# Relaxes those to 0644/0755, which every account on the machine can
|
||||
# read — leave it off if your web server and PHP are the same user.
|
||||
# FILES_WEB_SERVER_READABLE=true
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
CACHE_STORE=redis
|
||||
@@ -88,4 +97,3 @@ AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 402 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 403 KiB |
+45
-25
@@ -5,13 +5,33 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
paths:
|
||||
# Only the frontend is actually checked here, so only the frontend
|
||||
# needs to trigger it.
|
||||
- 'resources/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'eslint.config.js'
|
||||
- '.prettierrc*'
|
||||
- 'tsconfig.json'
|
||||
- '.github/workflows/lint.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
paths:
|
||||
- 'resources/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'eslint.config.js'
|
||||
- '.prettierrc*'
|
||||
- 'tsconfig.json'
|
||||
- '.github/workflows/lint.yml'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
# A second push supersedes the first.
|
||||
concurrency:
|
||||
group: linter-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
@@ -19,32 +39,32 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
php-version: '8.4'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
# community-modules resolves from its public GitHub repository (the vcs
|
||||
# entry in composer.json); cloud-modules is not required. COMPOSER_AUTH
|
||||
# just lifts the anonymous GitHub API rate limit for the fetch.
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}'
|
||||
run: |
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
npm install
|
||||
|
||||
- name: Run Pint
|
||||
run: vendor/bin/pint
|
||||
|
||||
- name: Format Frontend
|
||||
run: npm run format
|
||||
run: npm ci
|
||||
|
||||
# `eslint .` rather than `npm run lint`, which is `eslint . --fix`:
|
||||
# a formatter that rewrites the checkout and throws the result away
|
||||
# cannot fail a build, so it was never a gate. This one is.
|
||||
- name: Lint Frontend
|
||||
run: npm run lint
|
||||
run: npx eslint .
|
||||
|
||||
# - name: Commit Changes
|
||||
# uses: stefanzweifel/git-auto-commit-action@v5
|
||||
# with:
|
||||
# commit_message: fix code style
|
||||
# commit_options: '--no-verify'
|
||||
# Two steps used to live here and were removed on 2026-08-23, because
|
||||
# neither could ever fail:
|
||||
#
|
||||
# - `vendor/bin/pint`, without `--test` and with the auto-commit step
|
||||
# commented out. It reformatted the runner's checkout, exited 0 and
|
||||
# threw the result away — 150 seconds of this job's 195, gating
|
||||
# nothing. Reinstating it as a real gate means `pint --test`, which
|
||||
# today reports around a hundred pre-existing failures; the honest
|
||||
# order is a formatting sweep first, then the flag.
|
||||
#
|
||||
# - `npm run format`, which is `prettier --write`, for the same reason.
|
||||
# `prettier --check` currently reports 44 files, so the same applies:
|
||||
# sweep, then switch. `npm run format:check` is the command.
|
||||
#
|
||||
# Dropping them also let the PHP toolchain go: nothing left here needs it.
|
||||
|
||||
@@ -5,10 +5,50 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
paths-ignore:
|
||||
# Files no code reads and no test covers. Deliberately NOT listed:
|
||||
# CHANGELOG.md, which ReleaseNotes parses and ReleaseNotesTest
|
||||
# covers, and docs/, whose only two tracked files are served by
|
||||
# ApiDocsController and OpenApiController. A malformed edit to
|
||||
# either is exactly the thing that must not skip the suite.
|
||||
#
|
||||
# Repeated verbatim under pull_request: GitHub Actions does not
|
||||
# support YAML anchors.
|
||||
- 'README.md'
|
||||
- 'CONTRIBUTING.md'
|
||||
- 'SECURITY.md'
|
||||
- 'LICENSING.md'
|
||||
- 'CLA-ENTITY.md'
|
||||
- 'CLA-INDIVIDUAL.md'
|
||||
- 'INSTALL.md'
|
||||
- 'UPDATE.md'
|
||||
- 'DOCKER.md'
|
||||
- 'MIGRATING-FROM-V1.md'
|
||||
- 'docker/production/dockerhub-overview.md'
|
||||
- '.github/screenshots/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'CONTRIBUTING.md'
|
||||
- 'SECURITY.md'
|
||||
- 'LICENSING.md'
|
||||
- 'CLA-ENTITY.md'
|
||||
- 'CLA-INDIVIDUAL.md'
|
||||
- 'INSTALL.md'
|
||||
- 'UPDATE.md'
|
||||
- 'DOCKER.md'
|
||||
- 'MIGRATING-FROM-V1.md'
|
||||
- 'docker/production/dockerhub-overview.md'
|
||||
- '.github/screenshots/**'
|
||||
|
||||
# A second push supersedes the first: there is no value in finishing a run
|
||||
# for a commit nobody will look at again.
|
||||
concurrency:
|
||||
group: tests-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
@@ -87,5 +127,17 @@ jobs:
|
||||
- name: Static Analysis
|
||||
run: ./vendor/bin/phpstan analyse --no-progress
|
||||
|
||||
# `--parallel` rather than a shorter suite. One process took 191s of
|
||||
# this job's 4m30s; the same 1763 tests across the runner's cores
|
||||
# take about a third of that, with nothing skipped. paratest is
|
||||
# already a dev dependency (via Pest), so this needs no new install.
|
||||
#
|
||||
# `:memory:` explicitly: parallel testing gives each process its own
|
||||
# database, and an in-memory one per process is what the suite is
|
||||
# verified against locally. The job-level DB_DATABASE above is a file
|
||||
# path, which parallel workers would have to create and migrate
|
||||
# individually — a difference in behaviour with nothing to gain.
|
||||
- name: Tests
|
||||
run: ./vendor/bin/pest
|
||||
run: ./vendor/bin/pest --parallel
|
||||
env:
|
||||
DB_DATABASE: ':memory:'
|
||||
|
||||
+184
@@ -13,6 +13,190 @@ Anything under **Upgrade notes** is something you have to do, not something we d
|
||||
This section collects changes as they land; the release process turns it into a numbered entry when
|
||||
a version is cut.
|
||||
|
||||
### Added
|
||||
|
||||
- **Google Cloud Storage as a storage backend.** External storage used to mean S3 and nothing else.
|
||||
The Storage settings screen now asks which provider you are using first, and offers Google Cloud
|
||||
Storage alongside the S3-compatible option: choose it, paste a service account key with read and
|
||||
write access to your bucket, and new uploads go there. The key is stored encrypted and never shown
|
||||
again, and **Test connection** checks it can actually reach the bucket before you switch anything
|
||||
over — using a probe that works with a least-privilege key, rather than one that needs permission
|
||||
to read the bucket's own settings. Downloads and previews are handed to the visitor as a
|
||||
short-lived signed link, exactly as they already were for S3.
|
||||
|
||||
Nothing changes for an existing installation. Configurations saved before this release are S3, are
|
||||
still S3, and are not asked to say so. Files already stored stay where they are — the setting
|
||||
applies to new uploads, and there is still no migration between backends.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **An upload that cannot be stored now fails instead of disappearing.** When files are kept in
|
||||
object storage and the storage backend refuses a write — an expired key, a bucket that has been
|
||||
renamed or removed, a permission that changed underneath you — the upload used to report success
|
||||
and record the file anyway. The entry appeared in the file list, and the download it promised was
|
||||
never going to work, because the bytes had gone nowhere. The upload now stops and says so, and no
|
||||
file is recorded. Installations keeping files on local disk were never affected.
|
||||
|
||||
- **Downloads and thumbnails for installations using external storage.** Two places assumed every
|
||||
file sat on the server's own disk, which stopped being true the moment S3-compatible storage was
|
||||
switched on. A share link to a file held in a bucket produced a broken download, and a public
|
||||
listing could not draw a thumbnail for one at all — while the same file downloaded and previewed
|
||||
correctly everywhere else, which made it look like the share link or the listing was at fault
|
||||
rather than where the file lived. Both now read the file from wherever it actually is. Nothing
|
||||
changes for installations keeping files on local disk, which is most of them.
|
||||
|
||||
- **One confirmation message instead of two.** Saving a new client, system user or role showed the
|
||||
same green "Client created." twice, stacked. So did deleting one. It was only ever cosmetic —
|
||||
nothing happened twice — but it read as though something had, which is the last thing a
|
||||
confirmation should do. Saves that stay on the same screen, such as the email settings, were never
|
||||
affected.
|
||||
([#1675](https://github.com/projectsend/projectsend/issues/1675), reported and diagnosed by
|
||||
[@denkfabrik-li](https://github.com/denkfabrik-li))
|
||||
|
||||
- **Connecting a provider to an account that already has one.** Signing in with Google, Microsoft or
|
||||
a custom provider worked, but attaching one to an existing account did not: the **Connect** button
|
||||
on Settings → Connected accounts appeared to do nothing at all. The button asks the server in the
|
||||
background, and the server answered by redirecting to the provider — a redirect a browser will not
|
||||
follow out of a background request to another site. The page sat there with no consent screen and
|
||||
no error to explain it, so the only reading available was that the button was dead. The server now
|
||||
tells the browser to go to the provider itself, and the flow starts as it should. Signing in from
|
||||
the login page was never affected, and neither is it now.
|
||||
([#1676](https://github.com/projectsend/projectsend/pull/1676), found and fixed by
|
||||
[@denkfabrik-li](https://github.com/denkfabrik-li))
|
||||
|
||||
- **Downloads on a host where the web server is not PHP's user.** A download is not served by PHP:
|
||||
PHP checks permissions and then hands the web server the path to stream. Where the two run as
|
||||
different users — cPanel and Plesk commonly arrange it that way — the web server could not open
|
||||
the file, because uploads are written readable only by the account that wrote them. The rest of
|
||||
the site gave no sign of it: uploading worked, the library listed everything, and only downloads
|
||||
failed, in the browser as `ERR_INVALID_RESPONSE`. Setting `FILES_WEB_SERVER_READABLE=true` now
|
||||
writes uploads so the web server can read them. It is opt-in, and deliberately so — the modes it
|
||||
uses are readable by every account on the machine, which is the wrong trade on a server where the
|
||||
web server and PHP are the same user, as they are in the Docker image and on most servers people
|
||||
set up themselves. The install guide has the full procedure, including the one thing no
|
||||
application setting can fix: a PHP-FPM pool with a restrictive umask, which caps new directories
|
||||
no matter what ProjectSend asks for.
|
||||
([#1668](https://github.com/projectsend/projectsend/issues/1668), reported by
|
||||
[@denkfabrik-li](https://github.com/denkfabrik-li))
|
||||
|
||||
- **An installation that builds its own containers is no longer told to pull.** ProjectSend prints
|
||||
the update instructions for the way you installed it, and it had two answers where it needed
|
||||
three: anything running in a container was handed `docker compose pull && docker compose up -d`,
|
||||
including the Compose stack that builds from a checkout of the repository. There is no image
|
||||
behind those containers to pull, so both commands ran, reported success and changed nothing — and
|
||||
the dashboard went on offering the same release. Those installations are now recognised and given
|
||||
`git pull && docker compose up -d --build` instead, with the two extra steps a checkout needs when
|
||||
a release moves its dependencies or its frontend.
|
||||
([#1661](https://github.com/projectsend/projectsend/issues/1661), reported by
|
||||
[@mueller7382](https://github.com/mueller7382))
|
||||
|
||||
- **The dashboard no longer fails on shared hosting.** To decide which update instructions to print,
|
||||
ProjectSend asks whether it is running inside a container by looking for a file in the root of the
|
||||
filesystem. On shared hosting PHP is usually confined to your own directory, and looking outside it
|
||||
is treated as an error rather than as a "no" — so the one page that asks the question, the
|
||||
dashboard, returned a 500 while every other page worked. It now takes the restriction as the answer
|
||||
it always was: a server that keeps PHP inside a single directory is not our container image, and
|
||||
gets the manual update instructions, which is correct for shared hosting anyway. Nothing to change
|
||||
on your side, and no setting you would have been able to change if there were.
|
||||
([#1663](https://github.com/projectsend/projectsend/issues/1663), reported by
|
||||
[@denkfabrik-li](https://github.com/denkfabrik-li))
|
||||
|
||||
- **502 Bad Gateway behind a reverse proxy.** Every page carried a `Link:` header listing its
|
||||
frontend assets, duplicating tags the page already had in its `<head>` — twenty of them on the
|
||||
login screen, more on a heavier page. nginx buffers a response's headers into a single block that
|
||||
defaults to 4 KB, so the file list, at over 6 KB of headers, was refused with `upstream sent too
|
||||
big header` and the proxy answered 502. Which pages went over depended on how many assets they
|
||||
loaded, so it looked like an intermittent fault: the login screen appeared, and then the
|
||||
application did not. The duplicate header is gone — the same pages now send under 1.3 KB — and no
|
||||
browser loses anything, because the tags it actually reads were always in the document. The
|
||||
install guide gained the proxy buffer settings for anyone on an older version or behind a proxy
|
||||
holding a tighter default.
|
||||
([#1664](https://github.com/projectsend/projectsend/issues/1664), reported by
|
||||
[@denkfabrik-li](https://github.com/denkfabrik-li))
|
||||
|
||||
- **`docker logs` now shows the web server's log.** The container runs nginx, PHP-FPM, the queue
|
||||
worker and the scheduler, and all of them reported to Docker except the one you need when a
|
||||
request fails: nginx opened the log files named in its own configuration and wrote to them inside
|
||||
the container, where nothing looks. The effect was that a proxy problem produced no logs on either
|
||||
side — the reason for every 502 and every 403 existed, in a file nobody knew to open. Both its
|
||||
access and error logs now go to the container's output, and the Docker guide has a section on
|
||||
running behind a reverse proxy that says which side a given message points at.
|
||||
|
||||
## 2.1.0 — 18 August 2026
|
||||
|
||||
Updating, mostly. ProjectSend now tells you when there is a new version, ends an update somewhere
|
||||
rather than just stopping, and — on a server you run yourself — reduces the whole procedure to one
|
||||
command that asks before each step. This is also the first release published as an official Docker
|
||||
image.
|
||||
|
||||
### Added
|
||||
|
||||
- **Ask for an update the moment you want to know.** ProjectSend checks once a day on its own,
|
||||
which is no help to somebody who has just read that a release fixes the thing biting them. There
|
||||
is now a **Check now** button, and it says what it found rather than only that it ran.
|
||||
- **Updating ends somewhere.** The first time an administrator opens ProjectSend after an update,
|
||||
they land on a page saying which version they are now on and what came with it — and pointing at
|
||||
ProjectSend's Discord, which is where release news and help actually live.
|
||||
- **A first run that shows you around.** A brand-new installation greets its administrator once,
|
||||
with a short list of the things worth doing before real files go in. Steps tick themselves off as
|
||||
you do them.
|
||||
- **About says when this installation was last updated**, and to which version. It is the answer to
|
||||
"when did this change?", asked after something looks different or by whoever inherited the server.
|
||||
- **The database stops quietly filling up with things nobody reads.** Permanently failed emails and
|
||||
already-read notifications both grew forever. Both now have a retention window you set under
|
||||
**Housekeeping** on the Scheduler screen, with a nightly purge that honours it — thirty days and
|
||||
ninety by default, and `0` to keep everything. Unread notifications are never deleted, whatever
|
||||
their age.
|
||||
- **The update is in the activity log**, filterable like everything else. Until now the biggest
|
||||
change that can happen to an installation was the one thing its history did not record.
|
||||
- **One command to update, and it asks first.** *(Self-hosted)* `sudo ./update.sh` is the whole
|
||||
procedure: it offers to back up, verifies what it downloaded, and stops rather than guessing. Its
|
||||
useful options — take the backup for me, just tell me what would happen — are one click away on
|
||||
the update screen, and **[UPDATE.md](UPDATE.md)** documents the whole thing.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The browser tab takes its name from your site**, not from the build, and from the moment the
|
||||
page starts drawing rather than once it has loaded.
|
||||
- **Each edition points at its own home**, and hosted customers are no longer asked to donate to
|
||||
something they already pay for.
|
||||
- **The Scheduler screen names its tasks in your language.** Ten of them were English on an
|
||||
otherwise translated page.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A deleted folder, file or group no longer takes its name with it.** Deleting any of the three
|
||||
left the name reserved for good: creating another with that name failed with *"The slug has already been
|
||||
taken"*, naming a conflict with a row the interface will not show you, and there was no way to
|
||||
release it. Deleting now hands the name back, and names held by things you deleted earlier are
|
||||
released when you update.
|
||||
([#1645](https://github.com/projectsend/projectsend/issues/1645))
|
||||
- **The Legacy migration tool installs with the command the guide gives you.** It is published now,
|
||||
so `composer require projectsend/v1-migration-tool` works as written on any installation, with
|
||||
nothing to add to your `composer.json`. The guide also says plainly what a zip or Docker install
|
||||
can and cannot do — see the upgrade notes.
|
||||
- **A non-standard port no longer disappears from links** in email and on public pages.
|
||||
- **An update no longer stops over a symlink's ownership**, and the official image runs nginx as the
|
||||
user php-fpm writes as.
|
||||
- **Translations shipped by a package now reach the screen that wrote them.**
|
||||
|
||||
### Upgrade notes
|
||||
|
||||
- **Nothing is required beyond the usual update.** No new configuration, no new permissions to
|
||||
grant. The one database change runs itself.
|
||||
- **There is now an official Docker image**, `projectsend/projectsend`. If you have been building
|
||||
from a clone, you can switch to it: it ships with its dependencies and frontend already compiled,
|
||||
so it needs neither Composer nor Node. `compose.example.yaml` in `docker/production/` is a
|
||||
working starting point.
|
||||
- **Migrating from ProjectSend Legacy?** Read the first step of
|
||||
**[MIGRATING-FROM-V1.md](MIGRATING-FROM-V1.md)** before you start. It now differs by how you
|
||||
installed: a release zip and the official Docker image have no `/system/migrate` screen, because
|
||||
the frontend they ship was built before the tool existed, and the `projectsend:migrate:*` commands
|
||||
are the whole interface there. Nothing about the migration itself is missing.
|
||||
- **Reporting a security issue** now has a front door: the **Report a vulnerability** button on the
|
||||
repository's Security tab, or `contact@projectsend.org`. See
|
||||
[SECURITY.md](SECURITY.md).
|
||||
|
||||
## 2.0.0 — 2026-08-14
|
||||
|
||||
ProjectSend, rebuilt from the ground up. This is a new application rather than an update to the one
|
||||
|
||||
+10
-2
@@ -22,8 +22,10 @@ arrive without prior discussion are hard to review and often need rework.
|
||||
|
||||
## Setting up for development
|
||||
|
||||
The quickest way to a running copy is Docker — the steps that install ProjectSend are also the
|
||||
steps that set it up for development. From a fresh clone:
|
||||
A clone is a development copy, not an installation: `vendor/` and `public/build/` are deliberately
|
||||
not in git, so nothing runs until Composer and npm have filled them. That is what these steps do,
|
||||
and it is why the published Docker image — which ships both, already built — is what the README
|
||||
sends users to instead. From a fresh clone:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
@@ -49,6 +51,12 @@ A few things worth knowing:
|
||||
`docker compose --profile dev up -d adminer`.
|
||||
- On later boots the container migrates automatically. The manual `migrate` above is only needed on
|
||||
the first install, before `vendor/` exists.
|
||||
- Until `composer install` has run, the `worker` and `scheduler` containers have no application to
|
||||
run and exit with a message saying so; they pick themselves up once it has. If a page answers
|
||||
"ProjectSend is not installed yet" or "not built yet", it is naming the step that is missing.
|
||||
- Two checkouts of this repository share one Compose project name, so `docker compose up` in the
|
||||
second one takes over the first one's containers. Pass `-p some-other-name` when you want them
|
||||
side by side.
|
||||
|
||||
**Staff and clients are different things.** Staff — "system users" — administer the installation and
|
||||
upload files. Clients are the people files are shared with. There is no staff registration page:
|
||||
|
||||
@@ -8,20 +8,37 @@ data with it.
|
||||
|
||||
Read this before you put real files in ProjectSend, not after.
|
||||
|
||||
> Getting started with Docker in the first place is covered in [README](README.md#development).
|
||||
> **This page is about the official image**, `projectsend/projectsend`, started from the
|
||||
> `compose.example.yaml` in [Getting started](README.md#getting-started). That is the supported way
|
||||
> to run it.
|
||||
>
|
||||
> A **clone of this repository is a development copy, not an installation** — it builds from source,
|
||||
> bind-mounts the working tree, and ships nothing pre-built. If that is what you are running, its
|
||||
> setup and its data layout are [CONTRIBUTING.md](CONTRIBUTING.md), not this page.
|
||||
>
|
||||
> Installing without Docker, on a plain PHP server, is [INSTALL.md](INSTALL.md).
|
||||
|
||||
---
|
||||
|
||||
## The three things that matter
|
||||
## The two things that matter
|
||||
|
||||
Everything ProjectSend cannot regenerate lives in exactly three places:
|
||||
Everything ProjectSend cannot regenerate lives in exactly two Docker volumes:
|
||||
|
||||
| What | Where it is by default | Losing it means |
|
||||
|---|---|---|
|
||||
| **The database** | A Docker *named volume*, `projectsend_db-data` | Everything except the files themselves: accounts, groups, permissions, share links, comments, the activity log |
|
||||
| **Uploaded files** | `storage/app/files/` in the project directory | The files your clients downloaded — gone |
|
||||
| **`.env`** | The project directory | `APP_KEY`, without which saved SMTP and LDAP passwords cannot be decrypted |
|
||||
| **The database** | The volume mounted at `/var/lib/mysql` — `projectsend_db-data` | Everything except the files themselves: accounts, groups, permissions, share links, comments, the activity log |
|
||||
| **Uploaded files, and `APP_KEY`** | The volume mounted at `/var/www/html/storage` — `projectsend_storage` | The files your clients downloaded, and the key that decrypts saved SMTP and LDAP passwords |
|
||||
|
||||
The second one is the one people get wrong, because it is two things in one place. The container
|
||||
generates `.env` on first boot and keeps it *on the storage volume*, at `storage/.env`, symlinked
|
||||
into place — precisely so `APP_KEY` survives the container being replaced. A key that changes
|
||||
between restarts signs everybody out and makes every encrypted column permanently unreadable, and
|
||||
nothing errors when it happens. Back up the volume and you have both halves; back up only
|
||||
`storage/app/files/` and you have the files without the key.
|
||||
|
||||
(If you set `APP_KEY` in the environment instead, Laravel reads it from there and it wins. That is
|
||||
the right move when you already manage secrets somewhere else — but then it is *that* system's
|
||||
backup you are relying on.)
|
||||
|
||||
You do not have to work out which of these you have from memory. **The dashboard's System panel
|
||||
reports where your uploaded files actually live** — a host directory, a Docker volume (named), or
|
||||
@@ -36,14 +53,14 @@ Two things you may be surprised to find you do **not** need to protect:
|
||||
everyone out and drops any not-yet-sent emails or half-built zips. Annoying; not data loss.
|
||||
- **Parts of `storage/app/files/`** are derived, not precious: `zips/` (built downloads, deleted
|
||||
automatically after a day), `thumbnails/` and `previews/` (rebuilt on demand the next time
|
||||
somebody looks at a file). They sit in the same directory as the real uploads, so the simplest
|
||||
thing is to back up all of it and not think about which is which.
|
||||
somebody looks at a file). They sit inside the volume you are backing up anyway, so the simplest
|
||||
thing is to take all of it and not think about which is which.
|
||||
|
||||
## The good news, and the one command to fear
|
||||
|
||||
Named volumes are already outside the container lifecycle. `docker compose down`,
|
||||
`docker compose up --build`, deleting and recreating every container — none of those touch
|
||||
`projectsend_db-data`. Upgrading does not lose your database, and never did.
|
||||
Named volumes are already outside the container lifecycle. `docker compose pull`,
|
||||
`docker compose down`, deleting and recreating every container — none of those touch
|
||||
`projectsend_db-data` or `projectsend_storage`. Upgrading does not lose your data, and never did.
|
||||
|
||||
The command that *does* destroy it is:
|
||||
|
||||
@@ -52,73 +69,185 @@ docker compose down -v # ← the -v deletes the named volumes
|
||||
```
|
||||
|
||||
That flag exists to clean up a development machine. On a real installation it deletes your entire
|
||||
database in about a second, with no confirmation. The same goes for `docker volume prune` and
|
||||
`docker system prune --volumes` when the stack happens to be down.
|
||||
database and every uploaded file in about a second, with no confirmation. The same goes for
|
||||
`docker volume prune` and `docker system prune --volumes` when the stack happens to be down.
|
||||
|
||||
So the actual problem with the default setup is not fragility, it is **invisibility**: your
|
||||
database is somewhere under `/var/lib/docker/volumes/`, which means most people never back it up
|
||||
and would not know where to look. The rest of this page fixes that.
|
||||
So the actual problem with the default setup is not fragility, it is **invisibility**: your data is
|
||||
somewhere under `/var/lib/docker/volumes/`, which means most people never back it up and would not
|
||||
know where to look. The rest of this page fixes that.
|
||||
|
||||
---
|
||||
|
||||
## Surviving a reboot
|
||||
|
||||
Every service needs a restart policy, or the Docker daemon will not start it again when the host
|
||||
comes back:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
restart: unless-stopped
|
||||
db:
|
||||
restart: unless-stopped
|
||||
redis:
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
`compose.example.yaml` already has this on all three. It is worth checking if you wrote your own
|
||||
compose file, because the failure is silent and delayed: the stack works perfectly until the first
|
||||
reboot or power cut, and then the site is simply down with no error anywhere. `depends_on` does not
|
||||
cover this — it applies to `docker compose up`, not to containers the daemon brings back at boot.
|
||||
|
||||
```sh
|
||||
docker compose ps -a # after a reboot, everything should be Up, not Exited (0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Behind a reverse proxy
|
||||
|
||||
Almost nobody exposes the container directly: there is a proxy in front terminating TLS — Nginx
|
||||
Proxy Manager, Traefik, Caddy, or an nginx vhost you wrote. Two things are worth setting before you
|
||||
go looking for a bug that isn't there.
|
||||
|
||||
### Tell ProjectSend the proxy is there
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
TRUSTED_PROXIES: "*"
|
||||
```
|
||||
|
||||
Without it every visitor appears to come from the proxy. The login rate limiter then treats all of
|
||||
your users as one attacker, and the download log records the proxy's address instead of the
|
||||
person's. `compose.example.yaml` already sets this.
|
||||
|
||||
Leaving it unset does not cause a `502` — that means your proxy could not get a usable response out
|
||||
of the container at all, which is a different problem with a different fix. It does cause a **419
|
||||
"page expired"**. Without it the application never learns the proxy terminated TLS, so it builds
|
||||
its links and redirects with `http://` while the browser is on `https://`, and marks the session
|
||||
cookie as non-secure. The browser declines to send that cookie back to what it now reads as a
|
||||
different, less secure origin, the session arrives empty, and the first thing you submit — usually
|
||||
the create-your-admin-account form — is rejected as a stale token. After that you get returned to
|
||||
the login screen at random, because each redirect leaves and re-enters over the wrong scheme.
|
||||
|
||||
Your proxy also has to pass the original `Host` header through, or the links come out naming the
|
||||
container instead of your domain. Most do by default: `passHostHeader=true` in Traefik,
|
||||
`proxy_set_header Host $host;` in nginx.
|
||||
|
||||
### Give the proxy header headroom
|
||||
|
||||
If you are running a version before this one, some pages — the dashboard and the file list first —
|
||||
can send a response header block larger than the 4 KB single page nginx buffers headers into by
|
||||
default, and the proxy answers `502 Bad Gateway`. Because it depends on the page, it looks like an
|
||||
intermittent fault rather than a setting: the login screen loads, and then the application does not.
|
||||
The proxy's own error log names it exactly:
|
||||
|
||||
```
|
||||
upstream sent too big header while reading response header from upstream
|
||||
```
|
||||
|
||||
ProjectSend no longer sends headers that large. On an older version, or behind any proxy holding a
|
||||
default that tight, raise them:
|
||||
|
||||
```nginx
|
||||
proxy_buffer_size 32k;
|
||||
proxy_buffers 8 32k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
```
|
||||
|
||||
In Nginx Proxy Manager that goes in the **Advanced** tab of the proxy host. Traefik and Caddy have
|
||||
their own spellings; the idea is the same.
|
||||
|
||||
### When something does go wrong, read the container's log
|
||||
|
||||
The app container logs everything — nginx, PHP-FPM, the queue worker and the scheduler — to Docker:
|
||||
|
||||
```sh
|
||||
docker compose logs -f app
|
||||
docker compose logs --since 30m app | grep -iE "error|upstream|502"
|
||||
```
|
||||
|
||||
nginx's line is the one that matters for a proxy problem, because it says which side failed.
|
||||
`connect() failed` or `upstream timed out` means the request reached the container and PHP was the
|
||||
problem. **Nothing at all**, while your proxy reports a 502, means the request never arrived — look
|
||||
at the proxy, the network between them, and the published port, not at ProjectSend.
|
||||
|
||||
The container also answers a cheap health endpoint that touches neither the database nor Redis, which
|
||||
is the quickest way to separate "the app is down" from "the proxy cannot reach the app". Run both
|
||||
during an outage, from the same machine:
|
||||
|
||||
```sh
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://<host-ip>:8080/up # straight at the container
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://files.example.com/up
|
||||
```
|
||||
|
||||
Docker records the same check every 30 seconds, so there is a history to read after the fact:
|
||||
|
||||
```sh
|
||||
docker inspect --format 'restarts={{.RestartCount}} oom={{.State.OOMKilled}} health={{.State.Health.Status}}' $(docker compose ps -q app)
|
||||
```
|
||||
|
||||
A non-zero `restarts`, or `oom=true`, means the container is dying and coming back rather than
|
||||
misbehaving — check memory. `compose.example.yaml` sets no limits, and MySQL, Redis and up to ten
|
||||
PHP-FPM workers add up on a small VPS.
|
||||
|
||||
---
|
||||
|
||||
## Putting the data where you chose
|
||||
|
||||
Bind-mount both to real paths on the host, so your data sits somewhere you picked, somewhere you
|
||||
can see in `ls`, and somewhere your existing backup tool already knows about.
|
||||
Bind-mount both volumes to real paths on the host, so your data sits somewhere you picked, somewhere
|
||||
you can see in `ls`, and somewhere your existing backup tool already knows about.
|
||||
|
||||
### 1. Make the directories
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /srv/projectsend/files /srv/projectsend/mysql
|
||||
|
||||
# The app containers run as uid 1000 by default (the WWWUSER build argument).
|
||||
# If you set WWWUSER to something else in .env, use that instead.
|
||||
sudo chown -R 1000:1000 /srv/projectsend/files
|
||||
sudo mkdir -p /srv/projectsend/storage /srv/projectsend/mysql
|
||||
```
|
||||
|
||||
Leave `/srv/projectsend/mysql` owned by root — the MySQL image sets its own ownership the first
|
||||
time it starts.
|
||||
No `chown` needed for either. The ProjectSend container recreates the directory tree it needs on
|
||||
every boot and sets its own ownership (uid 1000), precisely because a bind-mounted host directory
|
||||
arrives empty where a named volume arrives seeded from the image. The MySQL image does the same for
|
||||
its own directory the first time it starts.
|
||||
|
||||
### 2. Create `compose.override.yaml`
|
||||
### 2. Point the compose file at them
|
||||
|
||||
Next to `compose.yaml`. Docker Compose reads this file automatically and merges it on top, so you
|
||||
never edit the tracked `compose.yaml` and nothing you write here is lost on the next update.
|
||||
`compose.example.yaml` is yours — you downloaded and edited it — so change the volumes in place
|
||||
rather than layering an override on top:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# All four app containers must see the same files directory. Missing one of
|
||||
# them is the classic mistake: uploads land in one place and downloads are
|
||||
# served from another, so every download 404s. `web` is the one people
|
||||
# forget — nginx serves the bytes itself, from
|
||||
# /var/www/html/storage/app/files/, so it needs the mount just as much as
|
||||
# the container that wrote them.
|
||||
app:
|
||||
volumes:
|
||||
- /srv/projectsend/files:/var/www/html/storage/app/files
|
||||
web:
|
||||
volumes:
|
||||
- /srv/projectsend/files:/var/www/html/storage/app/files
|
||||
worker:
|
||||
volumes:
|
||||
- /srv/projectsend/files:/var/www/html/storage/app/files
|
||||
scheduler:
|
||||
volumes:
|
||||
- /srv/projectsend/files:/var/www/html/storage/app/files
|
||||
# Was: storage:/var/www/html/storage
|
||||
- /srv/projectsend/storage:/var/www/html/storage
|
||||
|
||||
db:
|
||||
volumes:
|
||||
# Was: db-data:/var/lib/mysql
|
||||
- /srv/projectsend/mysql:/var/lib/mysql
|
||||
```
|
||||
|
||||
Check the result before applying it — this prints the fully merged configuration:
|
||||
Mount the whole `storage` directory, not `storage/app/files` inside it. Uploads are only half of
|
||||
what lives there — `storage/.env` holds `APP_KEY`, and mounting one level too deep leaves the key
|
||||
back inside the container where the next `docker compose down` takes it.
|
||||
|
||||
Then drop `storage:` and `db-data:` from the `volumes:` block at the bottom, if nothing else uses
|
||||
them, and check the result before applying it — this prints the fully merged configuration:
|
||||
|
||||
```sh
|
||||
docker compose config
|
||||
```
|
||||
|
||||
### 3. Move the data you already have
|
||||
### 3. Move an existing install's data onto the new paths
|
||||
|
||||
**Skip this on a brand-new installation.** There is nothing to move; go straight to step 4.
|
||||
This step is only for an install that has **already been running** on the named volumes and is now
|
||||
moving to the host paths you just chose. It moves ProjectSend's own storage and database, nothing
|
||||
else.
|
||||
|
||||
**Skip it on a brand-new installation** — there is nothing to move; go straight to step 4. That
|
||||
includes an install you are about to migrate ProjectSend Legacy (v1) into: those files and that
|
||||
database come across later, through the migration tool, and the new install has to be empty when
|
||||
they do. See [MIGRATING-FROM-V1.md](MIGRATING-FROM-V1.md).
|
||||
|
||||
Stop everything first. Copying a database out from under a running MySQL is how you get a backup
|
||||
that restores into a corrupt table.
|
||||
@@ -127,25 +256,22 @@ that restores into a corrupt table.
|
||||
docker compose down # no -v
|
||||
```
|
||||
|
||||
Files, which are already on the host inside the project directory:
|
||||
A throwaway container is the tidy way to reach inside a named volume:
|
||||
|
||||
```sh
|
||||
sudo rsync -a storage/app/files/ /srv/projectsend/files/
|
||||
sudo chown -R 1000:1000 /srv/projectsend/files
|
||||
```
|
||||
docker run --rm \
|
||||
-v projectsend_storage:/from \
|
||||
-v /srv/projectsend/storage:/to \
|
||||
alpine sh -c 'cd /from && cp -a . /to'
|
||||
|
||||
The database, which is in the named volume. A throwaway container is the tidy way to reach inside
|
||||
one:
|
||||
|
||||
```sh
|
||||
docker run --rm \
|
||||
-v projectsend_db-data:/from \
|
||||
-v /srv/projectsend/mysql:/to \
|
||||
alpine sh -c 'cd /from && cp -a . /to'
|
||||
```
|
||||
|
||||
(`projectsend_db-data` is the volume's real name — the `db-data` from `compose.yaml` prefixed with
|
||||
the project name. `docker volume ls` will confirm it.)
|
||||
(Those are the volumes' real names — the `storage` and `db-data` from your compose file, prefixed
|
||||
with the project name. `docker volume ls` will confirm them.)
|
||||
|
||||
### 4. Start, and check
|
||||
|
||||
@@ -155,14 +281,22 @@ docker compose up -d
|
||||
|
||||
Then prove it worked rather than assuming: log in and check the dashboard's System panel — **Files
|
||||
stored on** should now read *Host directory*, and the Docker-volume warning should be gone. Then
|
||||
open a file, **download it**, and upload a new one; confirm the new upload appears in
|
||||
`/srv/projectsend/files/` on the host. A download that returns nothing means one of the four
|
||||
containers is missing the mount from step 2.
|
||||
open a file, **download it**, and upload a new one; confirm the new upload appears under
|
||||
`/srv/projectsend/storage/app/files/` on the host.
|
||||
|
||||
Once you are satisfied, and not before, you can reclaim the old volume:
|
||||
Confirm the key came across too, since that is the half nothing on screen will tell you about:
|
||||
|
||||
```sh
|
||||
docker volume rm projectsend_db-data
|
||||
grep '^APP_KEY=' /srv/projectsend/storage/.env
|
||||
```
|
||||
|
||||
If that is empty or missing while your database has saved SMTP or LDAP credentials, stop and go
|
||||
back — the container will generate a *new* key and those passwords become unreadable.
|
||||
|
||||
Once you are satisfied, and not before, you can reclaim the old volumes:
|
||||
|
||||
```sh
|
||||
docker volume rm projectsend_storage projectsend_db-data
|
||||
```
|
||||
|
||||
---
|
||||
@@ -178,37 +312,41 @@ copy of a live data directory is not a snapshot — it is a set of files capture
|
||||
different moments, and it may restore into something subtly broken. Use a dump:
|
||||
|
||||
```sh
|
||||
docker compose exec -T db \
|
||||
mysqldump -u root -p"${DB_ROOT_PASSWORD:-root}" \
|
||||
docker compose exec -T db sh -c \
|
||||
'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" \
|
||||
--single-transaction --routines --triggers \
|
||||
projectsend > projectsend-$(date +%F).sql
|
||||
projectsend' > projectsend-$(date +%F).sql
|
||||
```
|
||||
|
||||
`--single-transaction` is what makes this safe on a running database: the dump sees one consistent
|
||||
moment in time without locking anybody out.
|
||||
moment in time without locking anybody out. Reading the password from the container's own
|
||||
environment keeps it off your shell history and off the process list on the host.
|
||||
|
||||
### The files
|
||||
### The files, and the key
|
||||
|
||||
```sh
|
||||
rsync -a /srv/projectsend/files/ /your/backup/location/files/
|
||||
rsync -a /srv/projectsend/storage/ /your/backup/location/storage/
|
||||
```
|
||||
|
||||
Ordinary files, no special handling. Restoring means copying them back and fixing ownership
|
||||
(`chown -R 1000:1000`).
|
||||
Ordinary files, no special handling — and taking the whole directory is what picks up `.env` with
|
||||
`APP_KEY` in it. That file is a few hundred bytes and it is the difference between a perfect backup
|
||||
and one where the SMTP and LDAP passwords in your database are undecryptable.
|
||||
|
||||
### `.env`
|
||||
If you kept the named volume instead of bind-mounting, the same content comes out through a
|
||||
throwaway container:
|
||||
|
||||
Copy it somewhere safe, once, and again whenever you change it. It is a few hundred bytes and it
|
||||
holds `APP_KEY` — lose that and the SMTP and LDAP passwords stored in your database become
|
||||
undecryptable, even though the rest of the backup is perfect.
|
||||
```sh
|
||||
docker run --rm -v projectsend_storage:/from -v "$PWD":/to \
|
||||
alpine tar czf /to/projectsend-storage-$(date +%F).tar.gz -C /from .
|
||||
```
|
||||
|
||||
### Restoring
|
||||
|
||||
```sh
|
||||
docker compose up -d db
|
||||
docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD:-root}" projectsend < projectsend-2026-08-08.sql
|
||||
sudo rsync -a /your/backup/location/files/ /srv/projectsend/files/
|
||||
sudo chown -R 1000:1000 /srv/projectsend/files
|
||||
docker compose exec -T db sh -c \
|
||||
'mysql -u root -p"$MYSQL_ROOT_PASSWORD" projectsend' < projectsend-2026-08-08.sql
|
||||
sudo rsync -a /your/backup/location/storage/ /srv/projectsend/storage/
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
@@ -222,14 +360,17 @@ restored is a hypothesis, not a backup.
|
||||
With the data outside the containers, an upgrade touches only the containers:
|
||||
|
||||
```sh
|
||||
docker compose down # again: no -v
|
||||
git pull # or unpack the new release over the directory
|
||||
docker compose up -d --build
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The app container migrates the database itself on boot and verifies its reference data, so there is
|
||||
no separate migration step. Take a database dump first anyway — migrations move forwards, not
|
||||
backwards, and the one time you skip it will be the time you want it.
|
||||
That is the whole procedure. The container runs `php artisan projectsend:update` itself on boot —
|
||||
the same command a manual install runs — so it migrates the database and verifies its reference data
|
||||
with no separate step. Take a database dump first anyway: migrations move forwards, not backwards,
|
||||
and the one time you skip it will be the time you want it.
|
||||
|
||||
**[UPDATE.md](UPDATE.md)** has the rest: what the container does on its way up, how to tell it
|
||||
worked, and what to do when it does not.
|
||||
|
||||
---
|
||||
|
||||
@@ -238,10 +379,14 @@ backwards, and the one time you skip it will be the time you want it.
|
||||
This is the payoff for everything above, and it is worth doing once deliberately so you know it
|
||||
works:
|
||||
|
||||
1. Dump the database and copy `/srv/projectsend/`, `.env` and the dump to the new machine.
|
||||
2. Install Docker, put the project directory in place, restore both as described under
|
||||
1. Dump the database, and copy `/srv/projectsend/` (or the storage tarball) and the dump to the new
|
||||
machine.
|
||||
2. Install Docker, put your `compose.yaml` in place, restore both as described under
|
||||
[Restoring](#restoring).
|
||||
3. Point DNS at the new machine, and update `APP_URL` in `.env` if the address changed.
|
||||
3. Point DNS at the new machine, and update `APP_URL` in your compose file if the address changed.
|
||||
|
||||
No export tool, no vendor involvement, nothing that only works while the old machine is alive.
|
||||
That is the property worth protecting, and the reason this page exists.
|
||||
Bring `APP_KEY` across with the storage directory — a fresh key on the new machine leaves the site
|
||||
working and the saved mail and LDAP passwords silently broken.
|
||||
|
||||
No export tool, no vendor involvement, nothing that only works while the old machine is alive. That
|
||||
is the property worth protecting, and the reason this page exists.
|
||||
|
||||
+151
-45
@@ -5,7 +5,7 @@ published with each release.
|
||||
|
||||
If you can run Docker, use Docker instead — it is one command, and everything on this page
|
||||
(PHP extensions, the web server, the background worker, the scheduled tasks) is already wired up
|
||||
for you. See [the Docker instructions](README.md#development), and
|
||||
for you. See [the Docker instructions](README.md#getting-started), and
|
||||
[DOCKER.md](DOCKER.md) for keeping your database and uploads outside the containers. Come back here
|
||||
if Docker is not an option on your hosting.
|
||||
|
||||
@@ -68,8 +68,23 @@ instruction PHP just gave. There is no setting to change; the header names simpl
|
||||
Two ways out, if nginx really is impossible on your hosting:
|
||||
|
||||
- Put nginx in front of Apache as a reverse proxy, serving `/protected-files/` itself. This works
|
||||
but is more moving parts than just using nginx.
|
||||
- Store your files in S3-compatible object storage instead (see
|
||||
but is more moving parts than just using nginx. Give the proxy some header headroom while you are
|
||||
there — the same headroom the reference configuration in Step 6 gives PHP-FPM, in the directives a
|
||||
proxy uses instead:
|
||||
|
||||
```nginx
|
||||
proxy_buffer_size 32k;
|
||||
proxy_buffers 8 32k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
```
|
||||
|
||||
nginx buffers a response's headers into a single block that defaults to one memory page — 4 KB on
|
||||
most systems — and answers `502 Bad Gateway` with `upstream sent too big header` when they do not
|
||||
fit. The page that goes over is not always the same one, so it presents as an intermittent fault
|
||||
rather than as a misconfiguration. This applies to any proxy in front of ProjectSend, not just
|
||||
this one: Nginx Proxy Manager, Traefik and a hand-written nginx vhost all ship the same default.
|
||||
([#1664](https://github.com/projectsend/projectsend/issues/1664))
|
||||
- Store your files in object storage instead — S3-compatible or Google Cloud Storage (see
|
||||
[Storing files somewhere other than this server](#storing-files-somewhere-other-than-this-server)).
|
||||
Files kept there are never on your server's disk, so downloads become a signed, expiring redirect
|
||||
to the storage provider and the web server is not involved at all. This is a genuine, supported
|
||||
@@ -182,6 +197,66 @@ sudo chown -R www-data:www-data /var/www/projectsend
|
||||
sudo chmod -R 775 /var/www/projectsend/storage /var/www/projectsend/bootstrap/cache
|
||||
```
|
||||
|
||||
### If your web server and PHP-FPM are different users
|
||||
|
||||
Check before you go further, because the symptom is misleading:
|
||||
|
||||
```sh
|
||||
ps -o user= -C nginx | sort -u # the web server's user
|
||||
ps -o user= -C php-fpm | sort -u # PHP's user
|
||||
```
|
||||
|
||||
Most servers you set up yourself run both as `www-data` and there is nothing to do here. Managed
|
||||
panels often do not — cPanel and Plesk commonly give each site its own PHP user while nginx runs as
|
||||
its own. If the two differ, add this to your `.env`:
|
||||
|
||||
```dotenv
|
||||
FILES_WEB_SERVER_READABLE=true
|
||||
```
|
||||
|
||||
Uploaded files are written `0600` inside `0700` directories, readable only by the user that wrote
|
||||
them. That is deliberate, and on a same-user server it is the safer setting. But a download is not
|
||||
served by PHP: PHP checks permissions and then hands the web server the path with `X-Accel-Redirect`
|
||||
(see [Why nginx](#why-nginx)), so the web server has to open a file PHP owns. When it cannot, **the
|
||||
whole site works and only downloads fail** — the browser reports `ERR_INVALID_RESPONSE` and the
|
||||
nginx error log says:
|
||||
|
||||
```
|
||||
open() ".../storage/app/files/..." failed (13: Permission denied)
|
||||
```
|
||||
|
||||
The setting relaxes new uploads to `0644`/`0755`. Be aware of what that means on a shared machine:
|
||||
those modes are readable by *every* account on the server, not only by the web server. The files stay
|
||||
off the web — the `internal` directive in Step 6 sees to that — but they are no longer private from
|
||||
your neighbours, so leave this off unless you need it.
|
||||
|
||||
Files already on disk keep the permissions they were written with, so fix those once:
|
||||
|
||||
```sh
|
||||
sudo find /var/www/projectsend/storage/app/files -type d -exec chmod 755 {} +
|
||||
sudo find /var/www/projectsend/storage/app/files -type f -exec chmod 644 {} +
|
||||
```
|
||||
|
||||
**Then check that new uploads keep it.** Upload a file and look at the directory it landed in:
|
||||
|
||||
```sh
|
||||
ls -ld /var/www/projectsend/storage/app/files/*/*
|
||||
```
|
||||
|
||||
If it is `drwxr-xr-x` you are done. If it is still `drwx------`, your PHP-FPM pool runs with a
|
||||
restrictive umask, and no application setting can beat it: ProjectSend asks for `0755`, but the
|
||||
directory is created by `mkdir()`, and `mkdir()` masks whatever mode it is given with the umask of
|
||||
the process. (Files are unaffected — they are set explicitly after being written, so they are `0644`
|
||||
either way.) Fix it in the pool configuration, not here:
|
||||
|
||||
```ini
|
||||
; /etc/php/8.4/fpm/pool.d/your-pool.conf — the path varies by panel
|
||||
php_admin_value[umask] = 0022
|
||||
```
|
||||
|
||||
Some panels expose this as a "umask" field instead. Restart PHP-FPM afterwards, then re-run the
|
||||
`chmod` above for anything uploaded in the meantime.
|
||||
|
||||
## Step 5 — Prepare the application
|
||||
|
||||
Three commands. Run them from the install directory, as the web server's user, so that everything
|
||||
@@ -275,7 +350,10 @@ edits the nginx config for you.
|
||||
|
||||
Open your site in a browser. Because no account exists yet, every address takes you to the setup
|
||||
screen, which asks for a site name and the name, email and password of the first administrator.
|
||||
Fill it in, and you are done — sign in and start adding clients.
|
||||
Fill it in, and you are done. The first time you sign in, ProjectSend opens on a short list of the
|
||||
things worth doing first — adding a client, uploading a file, choosing how your file lists and your
|
||||
email look — each one linking straight to the screen that does it. It appears once; afterwards it
|
||||
lives at **About → Getting started**.
|
||||
|
||||
If you would rather not do it in the browser (or you are scripting the install), the same thing
|
||||
from the command line:
|
||||
@@ -369,8 +447,20 @@ the worker afterwards.
|
||||
### Storing files somewhere other than this server
|
||||
|
||||
Out of the box, uploads live in `storage/app/files/` on this machine. You can point ProjectSend at
|
||||
S3-compatible object storage instead from **System → Settings → Storage** — useful when the files
|
||||
outgrow the server's disk.
|
||||
object storage instead from **System → Settings → Storage** — useful when the files outgrow the
|
||||
server's disk.
|
||||
|
||||
Two backends are offered. **S3-compatible** covers AWS S3 and everything speaking that API: MinIO,
|
||||
Backblaze B2, Wasabi, DigitalOcean Spaces. Leave the endpoint blank for AWS itself, or set it to the
|
||||
service's own address and turn on path-style addressing, which most of them need. **Google Cloud
|
||||
Storage** takes a service account key with read and write access to the bucket, pasted in as the JSON
|
||||
file Google issues; it is stored encrypted and never shown again.
|
||||
|
||||
Whichever you choose, use **Test connection** before switching uploads over — it checks the
|
||||
credentials actually reach the bucket, rather than leaving you to find out at the first upload.
|
||||
|
||||
The setting applies to new uploads. Files already on local disk stay there and keep working, and
|
||||
there is no migration between backends.
|
||||
|
||||
### Making it faster
|
||||
|
||||
@@ -382,53 +472,45 @@ sudo -u www-data php artisan view:cache
|
||||
sudo -u www-data php artisan event:cache
|
||||
```
|
||||
|
||||
Re-run them after every update. If you change your mind, `php artisan optimize:clear` undoes all
|
||||
three.
|
||||
You only run these once: `projectsend:update` notices they are in place and rebuilds them for you
|
||||
after every update. If you change your mind, `php artisan optimize:clear` undoes all three.
|
||||
|
||||
#### One command to skip: `config:cache`
|
||||
#### `config:cache` and your `.env`
|
||||
|
||||
Every Laravel deployment guide on the internet lists `php artisan config:cache` alongside those
|
||||
three, and `php artisan optimize` runs it for you. **Don't** — not on this application.
|
||||
Every Laravel deployment guide also lists `php artisan config:cache`, and `php artisan optimize`
|
||||
runs it for you. It is safe here, with one thing to remember.
|
||||
|
||||
Here is why. Caching the configuration writes every resolved setting into one PHP file, and from
|
||||
then on the framework stops reading your `.env` at all, on the entirely reasonable grounds that
|
||||
everything in it has already been baked in. That holds for settings read the normal way, through
|
||||
`config()`. ProjectSend reads one value earlier than that — `TRUSTED_PROXIES`, which has to be
|
||||
known before the middleware stack is assembled, so it is read straight from the environment. Cache
|
||||
the config and that read returns nothing.
|
||||
|
||||
Nothing breaks loudly. The site comes up, you log in, everything looks fine. But if there is a
|
||||
proxy or CDN in front of the server, ProjectSend goes back to believing every visitor is the proxy:
|
||||
the login rate limiter now counts all of your users as one attacker and locks the whole site out
|
||||
after five wrong passwords, and every row in the download log records the proxy's address instead
|
||||
of the person who actually downloaded the file. Both are the kind of thing you discover weeks
|
||||
later, from a complaint.
|
||||
|
||||
If you have already run it — or ran `php artisan optimize`, which includes it — `php artisan
|
||||
config:clear` puts things back immediately. The three commands above are safe and give you nearly
|
||||
all of the speed anyway; `config:cache` was always the smallest win of the four.
|
||||
Caching the configuration writes every resolved setting into one PHP file, and from then on the
|
||||
framework stops reading your `.env` at all — everything in it has already been baked in. So
|
||||
**re-run `php artisan config:cache` every time you edit `.env`**, or the edit does nothing and you
|
||||
are left staring at a setting that is plainly there and plainly ignored. `php artisan config:clear`
|
||||
goes back to reading `.env` directly, and every update clears it too, saying why.
|
||||
|
||||
---
|
||||
|
||||
## Updating to a new version
|
||||
|
||||
1. **Back up first** — the database, the `.env` file, and `storage/app/files/`. Every time.
|
||||
2. Put the site in maintenance mode: `sudo -u www-data php artisan down`
|
||||
3. Unpack the new zip over the install directory. Keep your `.env` and your `storage/` folder —
|
||||
the zip does not contain either, but check your unzip tool is not helpfully deleting things.
|
||||
4. Run the updates:
|
||||
```sh
|
||||
cd /var/www/projectsend
|
||||
sudo ./update.sh
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo -u www-data php artisan migrate --force
|
||||
sudo -u www-data php artisan projectsend:ensure-roles
|
||||
sudo -u www-data php artisan optimize:clear
|
||||
sudo -u www-data php artisan queue:restart
|
||||
```
|
||||
The script ships with every release, in this directory. It asks whether to check GitHub for a newer
|
||||
version, whether to download it (checking the published checksum), and whether you have a backup —
|
||||
then takes the site down, unpacks the release, migrates the database, rebuilds whichever caches you
|
||||
were using, reloads PHP-FPM, restarts the worker and brings the site back up. Your `.env`, your
|
||||
uploads and your `public/storage` link are never touched.
|
||||
|
||||
5. Bring it back: `sudo -u www-data php artisan up`
|
||||
`sudo ./update.sh --zip ~/projectsend-2.1.0.zip` applies a zip you downloaded yourself, and
|
||||
`./update.sh --check` just reports what is available.
|
||||
|
||||
`projectsend:ensure-roles` teaches the built-in roles about any permissions the new version added.
|
||||
It never touches permissions you have customised yourself.
|
||||
**[UPDATE.md](UPDATE.md)** is the full reference: what it does in order, every option, the same
|
||||
steps done by hand, how to check it worked, and how to go back. Read it before your first update —
|
||||
particularly the part about reloading PHP-FPM, which is the step that decides whether an update
|
||||
takes effect at all.
|
||||
|
||||
Back up first, every time. The script can dump the database for you (`--backup`), but your uploaded
|
||||
files in `storage/app/files/` are yours to look after.
|
||||
|
||||
---
|
||||
|
||||
@@ -474,19 +556,43 @@ report the problem on the settings screen instead. If you are stuck anyway, add
|
||||
`php artisan projectsend:captcha-off`. Your keys are kept either way. To check a key without
|
||||
locking anything, `php artisan projectsend:captcha-test` asks the provider directly.
|
||||
|
||||
**Links and redirects drop the port number** (you are served on `:8080`, and the site sends you to
|
||||
port 80).
|
||||
Recent Debian and Ubuntu nginx packages set `HTTP_HOST` to `$host` in `/etc/nginx/fastcgi_params`,
|
||||
deliberately — it stops a client-supplied `Host` header reaching the application — and `$host`
|
||||
carries no port. On 80 or 443 that changes nothing. On any other port, every absolute URL the
|
||||
application builds loses it. Add this to the `location ~ \.php$` block, **after** `include
|
||||
fastcgi_params;`:
|
||||
|
||||
```nginx
|
||||
fastcgi_param HTTP_HOST $http_host;
|
||||
```
|
||||
|
||||
On nginx 1.30 and later the safer form is `$host$is_request_port$request_port`. Either way this only
|
||||
applies to a non-standard port; behind a TLS proxy on 443 you do not need it.
|
||||
|
||||
**Emails and links point at `localhost` or the wrong domain.**
|
||||
`APP_URL` in `.env`. Fix it and run `php artisan optimize:clear`.
|
||||
|
||||
**A change I made in `.env` has no effect.**
|
||||
Run `php artisan optimize:clear`, then restart PHP-FPM and the worker. Both hold the old values
|
||||
until they are restarted. If it *still* has no effect, someone has run `php artisan config:cache`
|
||||
(or `optimize`) on this install — see [One command to skip](#one-command-to-skip-configcache).
|
||||
(or `optimize`) on this install — re-run it to pick the new value up, or `php artisan config:clear`
|
||||
to go back to reading `.env` directly.
|
||||
|
||||
**Everyone is locked out of the login form at once, or the download log shows the same IP for
|
||||
every download.**
|
||||
ProjectSend is seeing your proxy or CDN instead of your visitors. Set `TRUSTED_PROXIES` in `.env`
|
||||
(step 3) — and make sure `config:cache` has not been run, which stops that value from being read
|
||||
at all. Same section as above.
|
||||
(step 3) and restart PHP-FPM.
|
||||
|
||||
**Behind a reverse proxy: 419 "page expired" when you log in or save a form, or you land back on
|
||||
the login screen at random.**
|
||||
`TRUSTED_PROXIES` again (step 3). Without it ProjectSend never learns the proxy terminated TLS, so
|
||||
it builds its links and redirects with `http://` while the browser is on `https://`, and marks the
|
||||
session cookie as non-secure. The browser then declines to send that cookie back, the session
|
||||
arrives empty, and the write fails with a 419 that reads as an expired session. Make sure your
|
||||
proxy passes the original `Host` header through as well — `proxy_set_header Host $host;` in nginx,
|
||||
`passHostHeader=true` in Traefik (its default).
|
||||
|
||||
Still stuck? Ask in the [community forum](https://www.projectsend.org/) or open an issue on
|
||||
[GitHub](https://github.com/projectsend/projectsend/issues), and include the last few lines of
|
||||
|
||||
+83
-25
@@ -100,32 +100,74 @@ The tool reports each of these before it starts and names every affected row. It
|
||||
|
||||
## Step 1 — Install the tool
|
||||
|
||||
On the **new** install:
|
||||
Everything here happens on the **new** install. How the tool gets there — and whether you get its
|
||||
screen — depends on how ProjectSend itself got onto the machine, so find yours below and use that
|
||||
section on its own.
|
||||
|
||||
### If you installed from a release zip
|
||||
|
||||
```sh
|
||||
composer require projectsend/v1-migration-tool
|
||||
php artisan migrate # creates the tool's two tables
|
||||
npm run build # so its screen enters the frontend bundle
|
||||
```
|
||||
|
||||
In Docker, prefix each with `docker compose exec app` (except `npm run build`, which runs on the
|
||||
host).
|
||||
That is the whole installation — there is no `npm run build` to run. The zip ships its assets
|
||||
already compiled and deliberately without the toolchain that compiled them, so there is no
|
||||
`package.json` to build from.
|
||||
|
||||
> While the repository is private, `composer require` needs to be told where to find it — add a
|
||||
> `vcs` entry to your `composer.json` `repositories` and give Composer credentials for the repo:
|
||||
>
|
||||
> ```json
|
||||
> "repositories": [
|
||||
> { "type": "vcs", "url": "https://github.com/projectsend/v1-migration-tool" }
|
||||
> ]
|
||||
> ```
|
||||
One consequence is worth knowing before you go looking for it: **`/system/migrate` is not available
|
||||
on a zip install.** A package's screens enter the frontend bundle when that bundle is built, which
|
||||
for a zip is when the release was built — necessarily before this package was on your install.
|
||||
|
||||
Then open **`/system/migrate`** on your new install, signed in as a staff user with the *Edit
|
||||
settings* permission. There is no sidebar link — a one-time tool does not earn a permanent slot in
|
||||
the navigation of an install that will use it once.
|
||||
Nothing is missing from the migration itself. Every step below lists the command that does it, the
|
||||
commands do everything the screen does, and on a zip install they are the interface rather than a
|
||||
fallback. Start with [Step 2](#step-2--pick-your-route) and follow the commands.
|
||||
|
||||
Everything below can also be done entirely from the command line; the equivalent commands are
|
||||
listed at each step.
|
||||
### If you are running the official Docker image
|
||||
|
||||
The image carries no Composer. It ships the application already built and has no use for one, so
|
||||
fetch it for the length of the migration and use it in place:
|
||||
|
||||
```sh
|
||||
docker compose exec -u www-data app \
|
||||
php -r 'copy("https://getcomposer.org/composer.phar", "/tmp/composer.phar");'
|
||||
docker compose exec -u www-data app \
|
||||
php /tmp/composer.phar require projectsend/v1-migration-tool --update-no-dev
|
||||
docker compose exec -u www-data app php artisan migrate
|
||||
```
|
||||
|
||||
`-u www-data` is not decoration: `exec` lands as root, and a root-owned `vendor/` is a problem the
|
||||
application runs into later rather than now. `--update-no-dev` keeps the test tooling that the lock
|
||||
file knows about out of a production install.
|
||||
|
||||
That install lives in the container's writable layer, so it lasts until the container is replaced —
|
||||
a `docker compose pull`, or any change to your compose file, takes it with it. For a migration you
|
||||
finish in one sitting that is fine, and if it does go, install it again: the run's progress and its
|
||||
id map are in the database, not in the package.
|
||||
|
||||
There is no `/system/migrate` screen here either, for the same reason as a zip install. The commands
|
||||
are the interface — start with [Step 2](#step-2--pick-your-route).
|
||||
|
||||
### If you are running from a git checkout
|
||||
|
||||
Including the Docker stack in this repository, which builds the application from source rather than
|
||||
pulling the published image.
|
||||
|
||||
```sh
|
||||
composer require projectsend/v1-migration-tool
|
||||
php artisan migrate # creates the tool's two tables
|
||||
npm run build # so the tool's screen enters the frontend bundle
|
||||
```
|
||||
|
||||
In Docker, prefix the first two with `docker compose exec app`; `npm run build` runs on the host,
|
||||
where the toolchain is.
|
||||
|
||||
Then open **`/system/migrate`**, signed in as a staff user with the *Edit settings* permission.
|
||||
There is no sidebar link — a one-time tool does not earn a permanent slot in the navigation of an
|
||||
install that will use it once.
|
||||
|
||||
Everything the screen does can also be done entirely from the command line; the equivalent commands
|
||||
are listed at each step.
|
||||
|
||||
---
|
||||
|
||||
@@ -136,9 +178,11 @@ listed at each step.
|
||||
| Legacy and ProjectSend are on the **same machine** | [**Direct**](#step-3a--direct-same-machine) |
|
||||
| Legacy is on **another server**, or on hosting you cannot reach from the new box | [**Bundle**](#step-3b--bundle-different-machines) |
|
||||
|
||||
Direct is faster and simpler, and on a single filesystem it does not copy your files at all — it
|
||||
hardlinks them, so 400 GB migrates in seconds and both installs point at the same bytes until you
|
||||
decide otherwise. Use it if you can.
|
||||
Direct is faster and simpler. It copies your files by default, and it can also *hardlink* them
|
||||
instead when you ask it to — on a single filesystem that writes no bytes at all, so 400 GB migrates
|
||||
in seconds and both installs point at the same bytes until you decide otherwise. Either way your
|
||||
Legacy install is left intact. Use Direct if you can; [Step 3a](#step-3a--direct-same-machine) has
|
||||
the strategies.
|
||||
|
||||
---
|
||||
|
||||
@@ -151,7 +195,8 @@ Point the tool at your Legacy install directory. It reads the database credentia
|
||||
php artisan projectsend:migrate:preflight --v1-path=/var/www/projectsend-legacy
|
||||
```
|
||||
|
||||
On the screen, choose **Direct**, enter the same path, and pick how to move file bytes:
|
||||
If you have the screen, choose **Direct** and enter the same path. Either way, pick how to move
|
||||
file bytes:
|
||||
|
||||
| `--files=` | What it does |
|
||||
|---|---|
|
||||
@@ -168,8 +213,20 @@ If ProjectSend runs in Docker, the Legacy directory has to be visible **inside t
|
||||
## Step 3b — Bundle (different machines)
|
||||
|
||||
Run one dependency-free PHP file on the Legacy box; it produces a portable directory you bring
|
||||
over. Download it from `/system/migrate` (there is a link on the screen) or take it from the
|
||||
package at `bin/projectsend-v1-export.php`.
|
||||
over. Take it from the package, which is where it lives on every install:
|
||||
|
||||
```sh
|
||||
vendor/projectsend/v1-migration-tool/bin/projectsend-v1-export.php
|
||||
```
|
||||
|
||||
In Docker, copy it out of the container first:
|
||||
|
||||
```sh
|
||||
docker compose cp \
|
||||
app:/var/www/html/vendor/projectsend/v1-migration-tool/bin/projectsend-v1-export.php .
|
||||
```
|
||||
|
||||
If you have the screen, `/system/migrate` offers it as a download instead.
|
||||
|
||||
On the **Legacy** server:
|
||||
|
||||
@@ -207,8 +264,9 @@ rsync -a legacy-server:/var/www/projectsend/upload/files/ /tmp/ps-export/files/
|
||||
The import finds them there. If you would rather have everything in one object, export with
|
||||
`--files=copy` instead — convenient, and it doubles the disk you need on the Legacy box.
|
||||
|
||||
Then copy the bundle to the new server, choose **Bundle** on the screen and give it the path (again,
|
||||
the path *inside* the app container if you are using Docker):
|
||||
Then copy the bundle to the new server and give it the path — on the screen, choose **Bundle**;
|
||||
otherwise pass it directly. Either way it is the path *inside* the app container if you are using
|
||||
Docker:
|
||||
|
||||
```sh
|
||||
php artisan projectsend:migrate:preflight --bundle=/srv/ps-export
|
||||
|
||||
@@ -47,12 +47,12 @@ per-seat pricing. It runs on your server, and the files stay there.
|
||||
- 16 languages
|
||||
- A REST API with scoped tokens and generated OpenAPI docs
|
||||
- Privacy controls, including GDPR-grade account erasure with a grace period
|
||||
- Local disk or S3-compatible storage
|
||||
- Local disk, S3-compatible storage, or Google Cloud Storage
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/screenshots/dashboard.png" alt="The dashboard, showing counters for files, clients and groups alongside largest files, recent activity and system information" width="900">
|
||||
<img src=".github/screenshots/dashboard.png" alt="The dashboard: counters for files, clients and groups, the clients using the most storage against their quotas, a month of uploads and downloads as a line chart, and recent activity" width="900">
|
||||
</p>
|
||||
<p align="center"><em>The dashboard — what is in the installation, and what has been happening in it.</em></p>
|
||||
|
||||
@@ -68,17 +68,18 @@ per-seat pricing. It runs on your server, and the files stay there.
|
||||
|
||||
## Getting started
|
||||
|
||||
**With Docker** — the quickest path, and the one we recommend.
|
||||
**With Docker** — the quickest path, and the one we recommend. Nothing to build: the published
|
||||
image ships with its dependencies and its frontend already compiled.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/projectsend/projectsend.git
|
||||
cd projectsend
|
||||
cp .env.example .env # set PROJECTSEND_EDITION=community
|
||||
docker compose up -d
|
||||
curl -O https://raw.githubusercontent.com/projectsend/projectsend/main/docker/production/compose.example.yaml
|
||||
# edit the passwords and APP_URL in it, then:
|
||||
docker compose -f compose.example.yaml up -d
|
||||
```
|
||||
|
||||
The app is at `http://localhost:8090`, and the first thing it shows you is a setup screen that
|
||||
creates your administrator account.
|
||||
Open `APP_URL` and the first thing you see is a setup screen that creates your administrator
|
||||
account — or uncomment `ADMIN_EMAIL` and `ADMIN_PASSWORD` in the file first, with a password of
|
||||
your own, and it is created for you.
|
||||
|
||||
Before you put real files in it, read **[DOCKER.md](DOCKER.md)** — where your database and uploads
|
||||
actually live, how to move them onto paths you chose, and how to back them up so an upgrade can't
|
||||
@@ -89,6 +90,14 @@ take them with it.
|
||||
updating and troubleshooting. You do not need Composer or npm on the server; the zip ships ready to
|
||||
run.
|
||||
|
||||
**Already running it?** **[UPDATE.md](UPDATE.md)** is how you move to a new version — one command
|
||||
on Docker, one script on your own server, and what to check afterwards either way.
|
||||
|
||||
**Want to work on ProjectSend itself?** Cloning the repository gets you a development copy, not an
|
||||
installation: the dependencies and the compiled frontend are deliberately not in git, so a clone
|
||||
needs Composer and npm before it runs. **[CONTRIBUTING.md](CONTRIBUTING.md)** has the sequence, and
|
||||
it is short.
|
||||
|
||||
## Coming from ProjectSend Legacy?
|
||||
|
||||
The previous generation of ProjectSend lives on at
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
**Use [GitHub's private vulnerability reporting](https://github.com/projectsend/projectsend/security/advisories/new).**
|
||||
It is the "Report a vulnerability" button on this repository's Security tab. The report stays
|
||||
private between you and the maintainers, the whole exchange lives in one place, and it is the route
|
||||
that can end in a published advisory with a CVE and your name on it.
|
||||
|
||||
If you would rather not use GitHub, or the report does not fit that form, email
|
||||
<contact@projectsend.org> instead. Either is fine. What matters is that it does not start in
|
||||
public.
|
||||
|
||||
**Please do not open a public issue for a security report.** An issue is world-readable the moment
|
||||
it is filed, including by people running the version you just described how to break.
|
||||
|
||||
### What helps
|
||||
|
||||
Enough to reproduce it, and nothing you would not want to write down:
|
||||
|
||||
- The version, from **System → About** (self-hosted) or `config/projectsend.php`.
|
||||
- How the installation is deployed — the Docker image, a manual install behind nginx, something
|
||||
else — and anything unusual in front of it.
|
||||
- The steps, and what you saw. A short recording or a `curl` command beats a description.
|
||||
- What an attacker gets out of it, if it is not obvious.
|
||||
|
||||
You do not need a proof-of-concept exploit, and you should not run one against an installation that
|
||||
is not yours.
|
||||
|
||||
### What to expect
|
||||
|
||||
An acknowledgement within a few days, and a real answer — a fix, a plan, or a reason it is not
|
||||
what it looked like — once it has been reproduced. If a fix ships, you are credited by name unless
|
||||
you would rather not be.
|
||||
|
||||
This is a small project. If a week goes by in silence, assume the message went astray rather than
|
||||
that it was ignored, and send it again.
|
||||
|
||||
## What is in scope
|
||||
|
||||
Anything that lets somebody reach a file, an account, or an installation they should not: the
|
||||
sharing and permission rules, authentication and two-factor, the public pages and share links, the
|
||||
API, the upload and download paths, and the setup and update flows.
|
||||
|
||||
Some things are worth a report but are not vulnerabilities in ProjectSend:
|
||||
|
||||
- **An installation that has not done what the install guide says.** Serving the storage directory
|
||||
straight from the web server, or running without the protected-file rules, is a deployment
|
||||
problem — see [INSTALL.md](INSTALL.md) and [DOCKER.md](DOCKER.md). Tell us anyway if the
|
||||
documentation is what led somebody there.
|
||||
- **Findings from a scanner, unread.** A header a tool wanted and an exploit are different
|
||||
claims. Say which one you have.
|
||||
- **Anything in a dependency**, unless ProjectSend's use of it is what makes it reachable. Those
|
||||
belong upstream, and Dependabot already watches for them here.
|
||||
|
||||
## Supported versions
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **ProjectSend 2.x** | Supported. Fixes land on the current release line; upgrade before reporting that an older 2.x behaves differently. |
|
||||
| **The companion packages** — [`community-modules`](https://github.com/projectsend/community-modules), [`v1-migration-tool`](https://github.com/projectsend/v1-migration-tool) | Supported, on their own version lines. Report them here or on their own repository; both reach the same people. |
|
||||
| **ProjectSend Legacy (v1)** | A separate application in a separate repository — see [projectsend/legacy](https://github.com/projectsend/legacy) for how it handles reports. Nothing here applies to it. |
|
||||
|
||||
## Hardening your own installation
|
||||
|
||||
If you are trying to configure an installation rather than report a bug, the deployment
|
||||
documentation is what you want: [INSTALL.md](INSTALL.md) for a manual install, including the web
|
||||
server rules that keep uploaded files private, and [DOCKER.md](DOCKER.md) for the image, where
|
||||
those rules are already in place.
|
||||
|
||||
Two things are worth knowing whichever way you installed:
|
||||
|
||||
- **Put TLS in front of it**, and set `TRUSTED_PROXIES` when you do. Without it every visitor
|
||||
appears to come from the proxy, which turns the login rate limiter into one bucket for all of
|
||||
them and records the wrong address in the download log.
|
||||
- **`APP_KEY` decrypts what is already stored.** Back it up with the database, and do not rotate it
|
||||
on a running installation without knowing what you are doing.
|
||||
@@ -0,0 +1,258 @@
|
||||
# Updating ProjectSend
|
||||
|
||||
How to move an existing installation to a newer version, for both ways of running it. If you are
|
||||
installing for the first time, you want [Getting started](README.md#getting-started) for Docker or
|
||||
[INSTALL.md](INSTALL.md) for your own server instead.
|
||||
|
||||
Two rules hold everywhere in this document:
|
||||
|
||||
- **Back up first.** Every time, including the update you are sure about. Migrations move forwards,
|
||||
not backwards: there is no command that undoes them.
|
||||
- **Read [CHANGELOG.md](CHANGELOG.md) for the version you are moving to.** Anything a release needs
|
||||
beyond the steps below is written there. If a release raises the PHP requirement, that is where it
|
||||
says so.
|
||||
|
||||
Which path you are on decides the rest:
|
||||
|
||||
| How you installed | What updating means | Manual steps |
|
||||
|---|---|---|
|
||||
| The official Docker image (`projectsend/projectsend`) | Pull a new image, recreate the container | None — the container migrates itself |
|
||||
| Docker Compose built from a clone (CONTRIBUTING.md) | New code, rebuild the image | None — same entrypoint |
|
||||
| A release zip on your own server (INSTALL.md) | Download the zip, run one script | `sudo ./update.sh`, and answer three questions |
|
||||
|
||||
ProjectSend also tells you which of these you are on: the **System** card on the dashboard prints
|
||||
the update instructions for *this* server, and the notice that appears when a new version is
|
||||
released links to the same thing.
|
||||
|
||||
---
|
||||
|
||||
## Before you start, whichever path you are on
|
||||
|
||||
1. **Back up the database.**
|
||||
|
||||
```sh
|
||||
# Docker — --single-transaction is what makes this safe on a running database
|
||||
docker compose exec -T db \
|
||||
mysqldump -u root -p"${DB_ROOT_PASSWORD:-root}" \
|
||||
--single-transaction --routines --triggers \
|
||||
projectsend > projectsend-before-update.sql
|
||||
|
||||
# Your own server
|
||||
mysqldump -u projectsend -p --single-transaction --routines --triggers \
|
||||
projectsend > projectsend-before-update.sql
|
||||
```
|
||||
|
||||
2. **Back up the uploads and `.env`.** On Docker these live on the `storage` volume (or the host
|
||||
directory you pointed it at — see [DOCKER.md](DOCKER.md)); on a manual install they are
|
||||
`storage/app/files/` and `.env` in the install directory.
|
||||
|
||||
3. **Note the version you are on**, from the dashboard's System card. If you have to go back, that
|
||||
is the image tag or zip you go back to.
|
||||
|
||||
A backup that has never been restored is a hope, not a backup. If you have never tried, this is a
|
||||
good moment: restoring into a scratch database takes five minutes and tells you something real.
|
||||
|
||||
---
|
||||
|
||||
## Updating a Docker installation
|
||||
|
||||
### The official image
|
||||
|
||||
If you are using the published image — the `compose.example.yaml` shipped with it pins
|
||||
`projectsend/projectsend:2`, which follows every 2.x release — the whole update is:
|
||||
|
||||
```sh
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
That is the complete procedure. There is no migration step to run, no cache to clear, and nothing
|
||||
to do afterwards.
|
||||
|
||||
If you pinned an exact version instead (`projectsend/projectsend:2.0.0`), edit the tag in your
|
||||
compose file first — `pull` on a pinned tag fetches the same image you already have.
|
||||
|
||||
### Compose built from source
|
||||
|
||||
Same idea, one extra step because the image is yours to build:
|
||||
|
||||
```sh
|
||||
docker compose down # no -v, ever: -v deletes your data volumes
|
||||
git pull # or unpack the new release over the directory
|
||||
docker compose up -d --build
|
||||
docker compose exec app composer install # if composer.lock moved
|
||||
npm ci && npm run build # if package-lock.json or the frontend moved
|
||||
```
|
||||
|
||||
The last two lines are what a source checkout has that an image does not: its dependencies and its
|
||||
compiled frontend live outside git, so a release that changed either leaves them stale. If a page
|
||||
comes back saying ProjectSend is "not installed yet" or "not built yet", it is naming which of the
|
||||
two you skipped.
|
||||
|
||||
### What the container does on the way up
|
||||
|
||||
The entrypoint runs before the web server accepts a single request, in this order:
|
||||
|
||||
1. Recreates any missing `storage/` directories, and generates `APP_KEY` **only if one does not
|
||||
already exist** — yours is on the storage volume and is left alone. A changed key would make
|
||||
every existing session invalid and every encrypted column unreadable.
|
||||
2. Waits up to 60 seconds for the database to accept connections, so a slow-starting database is a
|
||||
pause rather than a crash loop.
|
||||
3. Runs `php artisan projectsend:update` — the same command a manual install runs, and the only
|
||||
definition of what an update does. It migrates the database, restores any built-in role a new
|
||||
version added a permission to (without touching roles you customised), relinks storage, clears
|
||||
the compiled caches and records the version it applied.
|
||||
4. Starts php-fpm, nginx, the queue worker and the scheduler.
|
||||
|
||||
The queue worker and scheduler run inside that same container, so they are replaced with it and
|
||||
never keep running old code.
|
||||
|
||||
### Checking it worked
|
||||
|
||||
```sh
|
||||
docker compose logs app | grep -A5 "Running migrations"
|
||||
docker compose ps # the app container should reach "healthy"
|
||||
```
|
||||
|
||||
The first time the administrator opens ProjectSend after an update, they land on a page naming the
|
||||
version now running and what the release brought, read from `CHANGELOG.md` inside the release
|
||||
itself. It appears once, for the account that administers the installation; afterwards it stays
|
||||
reachable from **About**, under the version line.
|
||||
|
||||
Then open the dashboard: the **System** card's *Version* line is the version now running, and the
|
||||
"a new version is available" notice disappears on its own once the running version has caught up —
|
||||
it compares against what is installed on every page load, so there is nothing to clear.
|
||||
|
||||
### If the container will not come up
|
||||
|
||||
The entrypoint stops on the first failure, so a container that restarts in a loop has told you why
|
||||
in its own log:
|
||||
|
||||
```sh
|
||||
docker compose logs app | tail -40
|
||||
```
|
||||
|
||||
The two common ones are a database that never became reachable (`DB_HOST`, credentials, or a
|
||||
database container that failed its healthcheck) and a migration that could not run. Neither leaves
|
||||
a half-updated site serving traffic: nginx is not started until the entrypoint finishes.
|
||||
|
||||
---
|
||||
|
||||
## Updating a manual installation
|
||||
|
||||
```sh
|
||||
cd /var/www/projectsend
|
||||
sudo ./update.sh
|
||||
```
|
||||
|
||||
That is the whole procedure. It asks three questions — whether to check GitHub for a newer release,
|
||||
whether to download it (verifying the checksum published beside it), and whether you have a backup —
|
||||
then does the rest. If you would rather fetch the zip yourself, hand it over instead:
|
||||
|
||||
```sh
|
||||
sudo ./update.sh --zip ~/projectsend-2.1.0.zip
|
||||
```
|
||||
|
||||
`./update.sh --check` reports what is installed and what is available, changes nothing, and does not
|
||||
need root.
|
||||
|
||||
### What it does, in order
|
||||
|
||||
1. Refuses to run inside a container, where updating means pulling a new image.
|
||||
2. Works out who your web server runs as, from the owner of `public/index.php`.
|
||||
3. Reads the version out of the zip and refuses to go backwards — migrations only move forwards —
|
||||
unless you pass `--force`. The *same* version is accepted, and is how you restore files that were
|
||||
modified or lost.
|
||||
4. Takes a database dump, if you asked for one (`--backup`).
|
||||
5. Puts the site into maintenance mode, and guarantees it comes back out: if anything fails, or you
|
||||
interrupt it, the site is brought back up before the script exits.
|
||||
6. Unpacks the release over your installation, leaving `.env`, `storage/` and `public/storage`
|
||||
alone, and replacing `vendor/` and `public/build/` wholesale rather than merging them.
|
||||
7. Runs `php artisan projectsend:update`, which migrates the database, restores the built-in roles,
|
||||
relinks storage, clears the compiled caches, rebuilds the optional ones **if you were using
|
||||
them**, and records the version it applied.
|
||||
8. Reloads PHP-FPM and restarts the queue worker.
|
||||
9. Brings the site back up and tells you what is running.
|
||||
|
||||
### Useful options
|
||||
|
||||
| Option | What for |
|
||||
|---|---|
|
||||
| `--zip <path>` | Apply a zip you downloaded yourself. |
|
||||
| `--backup` | Dump the database first, to `/var/backups/projectsend` (`--backup-dir` to change). |
|
||||
| `--check` | Report versions and stop. No root needed. |
|
||||
| `--user`, `--php-fpm`, `--worker` | Override what it detected. |
|
||||
| `--no-restart` | Leave systemd alone. You must then reload PHP-FPM yourself. |
|
||||
| `-y`, `--yes` | Unattended. Requires `--backup` or `--i-have-a-backup`. |
|
||||
| `--force` | Apply an older release. Read the sentence about migrations again first. |
|
||||
|
||||
### Doing it by hand
|
||||
|
||||
The script is not magic, and there is no harm in running the steps yourself:
|
||||
|
||||
```sh
|
||||
cd /var/www/projectsend
|
||||
sudo -u www-data php artisan down
|
||||
# unpack the release over this directory, keeping .env and storage/
|
||||
sudo chown -R www-data:www-data .
|
||||
sudo -u www-data php artisan projectsend:update
|
||||
sudo systemctl reload php8.4-fpm # not optional — see below
|
||||
sudo systemctl restart projectsend-worker
|
||||
sudo -u www-data php artisan up
|
||||
```
|
||||
|
||||
**The reload is the step that matters.** With OPcache configured the way production guides recommend
|
||||
(`opcache.validate_timestamps=0`, which our own Docker image uses), PHP does not re-read a file it
|
||||
has already compiled. Replacing the files changes nothing for the running site: the database ends up
|
||||
on the new version and the web server keeps serving the old code, while `php artisan` reports the
|
||||
new version the whole time you are trying to work out why.
|
||||
|
||||
If it happens anyway, ProjectSend now says so: staff see a banner naming the version being served,
|
||||
the version that was installed, and the command that fixes it.
|
||||
|
||||
---
|
||||
|
||||
## When it goes wrong
|
||||
|
||||
**The site shows the old version after updating.**
|
||||
Stale OPcache — reload PHP-FPM. `php artisan` reporting the new version while the browser reads the
|
||||
old one is this exact symptom, and staff now see a banner saying so, naming both versions and the
|
||||
command that fixes it. `sudo ./update.sh` does the reload for you unless you passed `--no-restart`.
|
||||
|
||||
**500 errors on every page, right after an update.**
|
||||
Usually compiled caches from the previous version. `php artisan optimize:clear`, then reload
|
||||
PHP-FPM. Check `storage/logs/` for the real error before assuming.
|
||||
|
||||
**"Base table or view not found" or a missing-column error.**
|
||||
The migrations did not run, or did not finish. Run `php artisan migrate --force` and read its output.
|
||||
On Docker, `docker compose logs app`.
|
||||
|
||||
**A setting looks stale — an old value the settings screen does not agree with.**
|
||||
`php artisan cache:clear` is safe at any time and fixes it.
|
||||
|
||||
**The background worker is still running old code.**
|
||||
It exits on `queue:restart` only once it finishes its current job, and something has to start it
|
||||
again. Check the service INSTALL.md sets up: `sudo systemctl status projectsend-worker`.
|
||||
|
||||
**Permission errors after unpacking.**
|
||||
Step 2's `chown`. `storage/` and `bootstrap/cache/` must be writable by the web server user — the
|
||||
same requirement as [INSTALL.md](INSTALL.md) step 4.
|
||||
|
||||
**You need to go back.**
|
||||
|
||||
1. Restore the code: the previous image tag (Docker), or the previous directory or zip (manual).
|
||||
2. Restore the database dump you took before starting.
|
||||
3. On a manual install, run `sudo -u www-data php artisan projectsend:update` afterwards, so the
|
||||
installation agrees with the code you restored. (A container does this itself on boot.) Until it
|
||||
runs, staff see the banner described above — which is correct: the database is ahead of the code.
|
||||
|
||||
Restore both, not one. A newer database against older code is the one combination nothing in this
|
||||
application expects, because migrations only ever move forwards.
|
||||
|
||||
---
|
||||
|
||||
## Version-specific notes
|
||||
|
||||
Anything a particular release needs beyond this document is in [CHANGELOG.md](CHANGELOG.md), under
|
||||
that version. It is worth reading before you start rather than after, particularly for a release
|
||||
that changes a requirement — the PHP floor, or a new extension.
|
||||
@@ -17,9 +17,11 @@ use App\Modules\Platform\Captcha\Captcha;
|
||||
use App\Modules\Platform\Installation\Installation;
|
||||
use App\Modules\Platform\Localization\LocaleRegistry;
|
||||
use App\Modules\Platform\Localization\TimezoneRegistry;
|
||||
use App\Modules\Platform\OfficialLinks;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use App\Modules\Platform\Updates\LatestReleaseInfo;
|
||||
use App\Modules\Platform\Updates\RunningCodeState;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
@@ -71,7 +73,10 @@ class HandleInertiaRequests extends Middleware
|
||||
'edition' => $capabilities->edition()->value,
|
||||
'noindex' => app(Settings::class)->get(Setting::DiscourageSearchIndexing),
|
||||
'version' => config('projectsend.version'),
|
||||
'links' => config('projectsend.links'),
|
||||
// Resolved rather than handed over raw: each edition has its
|
||||
// own front door, and a managed installation offers no
|
||||
// donation link at all. See OfficialLinks.
|
||||
'links' => app(OfficialLinks::class)->toArray(),
|
||||
// Whether the client- and visitor-facing surfaces name
|
||||
// ProjectSend. True everywhere unless a package answers
|
||||
// otherwise — see ResolvingAttribution. Staff surfaces
|
||||
@@ -96,6 +101,7 @@ class HandleInertiaRequests extends Middleware
|
||||
'password_policy' => app(PasswordPolicy::class)->descriptor(),
|
||||
'pending' => $this->pendingCounts($request),
|
||||
'update_notice' => $this->updateNotice($request),
|
||||
'code_notice' => $this->codeNotice($request),
|
||||
'locale' => app()->getLocale(),
|
||||
// The clock this viewer reads dates by, and whether it is a
|
||||
// choice or a fallback. The frontend needs both: the first to
|
||||
@@ -217,10 +223,44 @@ class HandleInertiaRequests extends Middleware
|
||||
: [...$release, 'install_kind' => app(Installation::class)->kind()->value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this process is running the code the installation was last
|
||||
* updated to — see RunningCodeState for the failure it catches.
|
||||
*
|
||||
* Gated on view_system_info rather than manage_updates: the latter is
|
||||
* edition-gated (Capability::SystemUpdates), and a server executing
|
||||
* code that does not match its own database is not a feature anyone
|
||||
* buys, it is a fact about the machine.
|
||||
*
|
||||
* @return array{reason: string, applied: string, running: string, applied_at: string, install_kind: string}|null
|
||||
*/
|
||||
protected function codeNotice(Request $request): ?array
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user === null || ! app(PermissionChecker::class)->allows($user, Permission::ViewSystemInfo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app(RunningCodeState::class)->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* App strings use English text as the translation key, so "en" ships no
|
||||
* messages — the key itself is the fallback.
|
||||
*
|
||||
* Asked of the framework's own loader rather than read out of
|
||||
* lang/{locale}.json directly, so that a package which registers its
|
||||
* catalogue with loadJsonTranslationsFrom() reaches the frontend too.
|
||||
* Reading the file worked for as long as every translatable string
|
||||
* belonged to this repository; the companion packages own screens of
|
||||
* their own, and theirs were rendering in English in every language
|
||||
* because their catalogue never got this far.
|
||||
*
|
||||
* Precedence comes from the loader and is the useful way round: an
|
||||
* installation's own lang/{locale}.json is merged last and therefore
|
||||
* wins, so a package string can be overridden locally.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function translations(string $locale): array
|
||||
@@ -229,13 +269,7 @@ class HandleInertiaRequests extends Middleware
|
||||
return [];
|
||||
}
|
||||
|
||||
$path = lang_path("{$locale}.json");
|
||||
|
||||
if (! is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var array<string, string> */
|
||||
return json_decode((string) file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
|
||||
return app('translator')->getLoader()->load($locale, '*', '*');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Modules\Platform\Onboarding\InstallationWelcome;
|
||||
use App\Modules\Platform\Updates\UpdateWelcome;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Takes the administrator to whichever page is waiting for them: the
|
||||
* getting-started list on a new installation, or the what's-new page the
|
||||
* first time they arrive after an update.
|
||||
*
|
||||
* Attached to the dashboard alone rather than to the whole web group.
|
||||
* The dashboard is where a login lands and where the sidebar's logo
|
||||
* points, so it catches the arrival either way — including the common
|
||||
* case on a self-hosted server, where the person who ran update.sh was
|
||||
* already signed in and never logs in at all. Applying it to every
|
||||
* request instead would mean intercepting somebody mid-download or
|
||||
* mid-upload to congratulate them, which is a worse trade than missing
|
||||
* an administrator who happens to bookmark /files.
|
||||
*
|
||||
* One middleware for both because they are the same interruption, and a
|
||||
* second one on the same route would have to know about the first to
|
||||
* avoid arguing with it. Installation wins: the two cannot both be
|
||||
* waiting in practice — an update marker is only raised for an
|
||||
* installation that already existed — but if they ever were, somebody
|
||||
* who has just installed this does not need release notes for a version
|
||||
* they never ran.
|
||||
*/
|
||||
class RedirectToGreeting
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InstallationWelcome $installation,
|
||||
private readonly UpdateWelcome $update,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// GET only: a redirect swallows a POST body, and nothing that
|
||||
// writes should ever be answered with a greeting.
|
||||
if ($request->isMethod('GET')) {
|
||||
$user = $request->user();
|
||||
|
||||
if ($user !== null) {
|
||||
if ($this->installation->isWaitingFor($user)) {
|
||||
return redirect()->route('system.getting-started');
|
||||
}
|
||||
|
||||
if ($this->update->isWaitingFor($user)) {
|
||||
return redirect()->route('system.whats-new');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ enum Action: string
|
||||
// Platform / lifecycle (v1 action 0: "ProjectSend has been installed")
|
||||
case SetupCompleted = 'setup.completed';
|
||||
case SettingsUpdated = 'settings.updated';
|
||||
case ApplicationUpdated = 'application.updated';
|
||||
|
||||
// Identity
|
||||
case Login = 'auth.login';
|
||||
@@ -53,6 +54,7 @@ enum Action: string
|
||||
case ShareLinkRevoked = 'share_link.revoked';
|
||||
case ShareLinkDownloaded = 'share_link.downloaded';
|
||||
case PublicFileDownloaded = 'public_file.downloaded';
|
||||
case PublicFilePreviewed = 'public_file.previewed';
|
||||
case FolderCreated = 'folder.created';
|
||||
case FolderRenamed = 'folder.renamed';
|
||||
case FolderMoved = 'folder.moved';
|
||||
@@ -142,6 +144,7 @@ enum Action: string
|
||||
{
|
||||
return match ($this) {
|
||||
self::SetupCompleted => 'Installed ProjectSend',
|
||||
self::ApplicationUpdated => 'Updated ProjectSend to :to, from :from',
|
||||
self::SettingsUpdated => 'Updated the system settings (:section)',
|
||||
self::Login => 'Logged in',
|
||||
self::Logout => 'Logged out',
|
||||
@@ -178,6 +181,7 @@ enum Action: string
|
||||
self::ShareLinkRevoked => 'Revoked a public link for the file ":subject"',
|
||||
self::ShareLinkDownloaded => 'Downloaded the file ":subject" via a public link',
|
||||
self::PublicFileDownloaded => 'Downloaded the file ":subject" via the public group listing',
|
||||
self::PublicFilePreviewed => 'Previewed the file ":subject" via the public group listing',
|
||||
self::FolderCreated => 'Created the folder ":subject"',
|
||||
self::FolderRenamed => 'Renamed the folder ":subject"',
|
||||
self::FolderMoved => 'Moved the folder ":subject"',
|
||||
@@ -242,6 +246,7 @@ enum Action: string
|
||||
{
|
||||
return match ($this) {
|
||||
self::SetupCompleted => 'ProjectSend was installed',
|
||||
self::ApplicationUpdated => 'ProjectSend was updated to a new version',
|
||||
self::SettingsUpdated => 'System settings were updated',
|
||||
self::Login => 'Logged in',
|
||||
self::Logout => 'Logged out',
|
||||
@@ -275,6 +280,7 @@ enum Action: string
|
||||
self::ShareLinkRevoked => 'A public link was revoked',
|
||||
self::ShareLinkDownloaded => 'A file was downloaded via a public link',
|
||||
self::PublicFileDownloaded => 'A file was downloaded via the public group listing',
|
||||
self::PublicFilePreviewed => 'A file was previewed via the public group listing',
|
||||
self::FolderCreated => 'A folder was created',
|
||||
self::FolderRenamed => 'A folder was renamed',
|
||||
self::FolderMoved => 'A folder was moved',
|
||||
|
||||
@@ -93,7 +93,7 @@ class ActivityLogger
|
||||
|
||||
private function shouldRecordIp(Action $action, ?User $actor): bool
|
||||
{
|
||||
if (! in_array($action, [Action::FileDownloaded, Action::FilePreviewed, Action::ShareLinkDownloaded, Action::PublicFileDownloaded], true)) {
|
||||
if (! in_array($action, [Action::FileDownloaded, Action::FilePreviewed, Action::ShareLinkDownloaded, Action::PublicFileDownloaded, Action::PublicFilePreviewed], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,17 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Audit\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLog;
|
||||
use App\Modules\Audit\ActivityLogScope;
|
||||
use App\Modules\Audit\DownloadPresenter;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Modules\Platform\Localization\LocalDay;
|
||||
use App\Modules\Platform\Localization\TimezoneRegistry;
|
||||
use App\Support\Pagination;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -27,6 +32,7 @@ class DownloadsController extends Controller
|
||||
public function __construct(
|
||||
private readonly DownloadPresenter $presenter,
|
||||
private readonly ActivityLogScope $scope,
|
||||
private readonly TimezoneRegistry $timezones,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
@@ -34,15 +40,9 @@ class DownloadsController extends Controller
|
||||
$viewer = $request->user();
|
||||
assert($viewer !== null);
|
||||
|
||||
// A download row names the file and says who fetched it from which
|
||||
// IP, so it needs the viewer's library scope applied — not just
|
||||
// `view_actions_log`. See ActivityLogScope for the full reasoning.
|
||||
$entries = $this->scope
|
||||
->apply(ActivityLog::query(), $viewer)
|
||||
->where('subject_type', (new File)->getMorphClass())
|
||||
->whereIn('action', [Action::FileDownloaded, Action::ShareLinkDownloaded, Action::PublicFileDownloaded])
|
||||
->orderByDesc('created_at')
|
||||
->orderByDesc('id')
|
||||
$filters = $this->validatedFilters($request);
|
||||
|
||||
$entries = $this->filteredQuery($filters, $viewer)
|
||||
->paginate(25)
|
||||
->withQueryString();
|
||||
|
||||
@@ -63,6 +63,65 @@ class DownloadsController extends Controller
|
||||
];
|
||||
})->all(),
|
||||
'pagination' => Pagination::meta($entries),
|
||||
'filters' => $filters,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{file: ?string, user: ?string, from: ?string, to: ?string}
|
||||
*/
|
||||
private function validatedFilters(Request $request): array
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'file' => ['nullable', 'string', 'max:255'],
|
||||
'user' => ['nullable', 'string', 'max:255'],
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date', 'after_or_equal:from'],
|
||||
]);
|
||||
|
||||
return [
|
||||
'file' => $validated['file'] ?? null,
|
||||
'user' => $validated['user'] ?? null,
|
||||
'from' => $validated['from'] ?? null,
|
||||
'to' => $validated['to'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{file: ?string, user: ?string, from: ?string, to: ?string} $filters
|
||||
* @return Builder<ActivityLog>
|
||||
*/
|
||||
private function filteredQuery(array $filters, User $viewer): Builder
|
||||
{
|
||||
$timezone = $this->timezones->resolve($viewer);
|
||||
|
||||
// A download row names the file and says who fetched it from which
|
||||
// IP, so it needs the viewer's library scope applied — not just
|
||||
// `view_actions_log`. See ActivityLogScope for the full reasoning.
|
||||
return $this->scope
|
||||
->apply(ActivityLog::query(), $viewer)
|
||||
->where('subject_type', (new File)->getMorphClass())
|
||||
->whereIn('action', [Action::FileDownloaded, Action::ShareLinkDownloaded, Action::PublicFileDownloaded])
|
||||
// Both names are matched on what the entry snapshotted, not on
|
||||
// a join: a file or an account deleted since is still findable
|
||||
// by the name it went out under, which is often exactly what
|
||||
// this page is being asked.
|
||||
->when($filters['file'], fn (Builder $query, string $file) => $query->where('subject_name', 'like', "%{$file}%"))
|
||||
// Only rows with a real account can match a name. The two
|
||||
// anonymous flavours ("Public link", "Public listing") are
|
||||
// labels this page prints, not stored values, so a search for
|
||||
// them finds nothing rather than something arbitrary.
|
||||
->when($filters['user'], fn (Builder $query, string $user) => $query->where('actor_name', 'like', "%{$user}%"))
|
||||
// The viewer's own calendar day, not the UTC one — see LocalDay.
|
||||
->when(
|
||||
$filters['from'] !== null ? LocalDay::start($filters['from'], $timezone) : null,
|
||||
fn (Builder $query, Carbon $from) => $query->where('created_at', '>=', $from),
|
||||
)
|
||||
->when(
|
||||
$filters['to'] !== null ? LocalDay::end($filters['to'], $timezone) : null,
|
||||
fn (Builder $query, Carbon $to) => $query->where('created_at', '<=', $to),
|
||||
)
|
||||
->orderByDesc('created_at')
|
||||
->orderByDesc('id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class ClientSettingsController extends Controller
|
||||
'clients_can_select_group' => $this->settings->get(Setting::ClientsCanSelectGroup),
|
||||
'clients_membership_deny_cooldown_days' => $this->settings->get(Setting::ClientsMembershipDenyCooldownDays),
|
||||
'default_client_storage_quota_mb' => (int) $this->settings->get(Setting::DefaultClientStorageQuotaMb),
|
||||
'clients_can_preview_files' => $this->settings->get(Setting::ClientsCanPreviewFiles),
|
||||
'groups' => Group::query()->orderBy('name')->get()
|
||||
->map(fn (Group $group): array => ['id' => $group->id, 'name' => $group->name])
|
||||
->all(),
|
||||
@@ -47,6 +48,7 @@ class ClientSettingsController extends Controller
|
||||
'clients_can_select_group' => ['required', Rule::in(['none', 'public'])],
|
||||
'clients_membership_deny_cooldown_days' => ['required', 'integer', 'min:0', 'max:365'],
|
||||
'default_client_storage_quota_mb' => ['required', 'integer', 'min:0'],
|
||||
'clients_can_preview_files' => ['required', 'boolean'],
|
||||
]);
|
||||
|
||||
$this->settings->set(Setting::ClientsCanRegister, $validated['clients_can_register']);
|
||||
@@ -55,6 +57,7 @@ class ClientSettingsController extends Controller
|
||||
$this->settings->set(Setting::ClientsCanSelectGroup, $validated['clients_can_select_group']);
|
||||
$this->settings->set(Setting::ClientsMembershipDenyCooldownDays, (int) $validated['clients_membership_deny_cooldown_days']);
|
||||
$this->settings->set(Setting::DefaultClientStorageQuotaMb, (int) $validated['default_client_storage_quota_mb']);
|
||||
$this->settings->set(Setting::ClientsCanPreviewFiles, $validated['clients_can_preview_files']);
|
||||
|
||||
$this->activity->log(Action::SettingsUpdated, context: ['section' => 'clients']);
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Files\Delivery;
|
||||
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Support\ContentDisposition;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* A stored file's own bytes, put on the wire for whichever disk it lives
|
||||
* on.
|
||||
*
|
||||
* Every route that hands over a file reaches this after authorizing in
|
||||
* its own way — a policy, a share token, a public-listing check. It
|
||||
* authorizes nothing itself, and deliberately knows nothing about who is
|
||||
* asking. The one thing it knows is the thing each caller kept getting
|
||||
* wrong on its own: that `$file->disk` decides how the bytes travel.
|
||||
*
|
||||
* Local disk: X-Accel-Redirect, so nginx streams the file and PHP never
|
||||
* touches the bytes. Anything else — S3, GCS and friends — gets a
|
||||
* short-lived presigned URL carrying the disposition, which an object
|
||||
* store ranges just as well.
|
||||
*
|
||||
* That distinction matters most for inline(): a <video> seeking through
|
||||
* an hour of footage issues a long tail of Range requests, and nginx's
|
||||
* static handler answers those with 206s on its own, dropping the
|
||||
* Content-Length below in favour of the range it actually served.
|
||||
*
|
||||
* Callers of inline() must have established that the mime type is
|
||||
* inline-safe first; PreviewKind is the allowlist, and the reason there
|
||||
* is one.
|
||||
*/
|
||||
class StoredFileResponse
|
||||
{
|
||||
/** Shown in place — a preview. */
|
||||
public function inline(File $file): Response|RedirectResponse
|
||||
{
|
||||
return $this->make($file, ContentDisposition::inline($file->original_name));
|
||||
}
|
||||
|
||||
/** Handed over — a download. */
|
||||
public function attachment(File $file): Response|RedirectResponse
|
||||
{
|
||||
return $this->make($file, ContentDisposition::attachment($file->original_name));
|
||||
}
|
||||
|
||||
private function make(File $file, string $disposition): Response|RedirectResponse
|
||||
{
|
||||
if ($file->disk !== 'files') {
|
||||
$url = Storage::disk($file->disk)->temporaryUrl(
|
||||
$file->path,
|
||||
now()->addHour(),
|
||||
['ResponseContentDisposition' => $disposition],
|
||||
);
|
||||
|
||||
return redirect()->away($url);
|
||||
}
|
||||
|
||||
return response('', 200, [
|
||||
'X-Accel-Redirect' => '/protected-files/'.$file->path,
|
||||
'Content-Type' => $file->mime_type,
|
||||
'Content-Disposition' => $disposition,
|
||||
'Content-Length' => (string) $file->size,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,18 @@ class ChunkedUploadsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return response('', 200, ['ETag' => '"'.$etag.'"']);
|
||||
return response('', 200, [
|
||||
'ETag' => '"'.$etag.'"',
|
||||
// Stated rather than left to Symfony, whose default for an
|
||||
// empty body is text/html. A CDN that rewrites HTML — as
|
||||
// Cloudflare's Email Obfuscation and Automatic HTTPS Rewrites
|
||||
// do — drops the origin's ETag from an HTML response, since a
|
||||
// rewritten body would no longer match it. Nothing on this side
|
||||
// would notice (LocalPartStore keeps its own record of every
|
||||
// part), but the client never learns the part landed, so the
|
||||
// upload stalls at 100% with no error anywhere.
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Files\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLog;
|
||||
use App\Modules\Audit\ActivityPresenter;
|
||||
@@ -18,10 +19,15 @@ use App\Modules\Files\Models\File;
|
||||
use App\Modules\Files\Models\Folder;
|
||||
use App\Modules\Files\Models\ShareLink;
|
||||
use App\Modules\Files\Versions\FileVersionLinks;
|
||||
use App\Modules\Platform\Localization\LocalDay;
|
||||
use App\Modules\Platform\Localization\TimezoneRegistry;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@@ -34,6 +40,41 @@ class FileDetailsController extends Controller
|
||||
/** Raw rows considered when grouping downloads() by actor — see that method's docblock. */
|
||||
private const DOWNLOADS_SUMMARY_LIMIT = 500;
|
||||
|
||||
/**
|
||||
* How a file leaves: three actions, because *how* it left matters —
|
||||
* a signed-in recipient, somebody following a public link, and a
|
||||
* visitor to a public group listing are all recorded separately.
|
||||
*
|
||||
* @var non-empty-list<Action>
|
||||
*/
|
||||
private const DOWNLOAD_ACTIONS = [Action::FileDownloaded, Action::ShareLinkDownloaded, Action::PublicFileDownloaded];
|
||||
|
||||
/**
|
||||
* Looking at a file without taking it. One action today; if a second
|
||||
* way to preview is ever recorded separately, add it here and give
|
||||
* previews an ACTION_GROUPS entry the way downloads has one.
|
||||
*
|
||||
* @var non-empty-list<Action>
|
||||
*/
|
||||
private const PREVIEW_ACTIONS = [Action::FilePreviewed];
|
||||
|
||||
/**
|
||||
* Filters that stand for a question rather than for one logged action.
|
||||
*
|
||||
* Nobody reading a file's history wants to ask "who downloaded this?"
|
||||
* three times, so this offers it once — and only when the file's own
|
||||
* log holds more than one of the members, since otherwise it would
|
||||
* filter to exactly what its single member already offers.
|
||||
*
|
||||
* @var array<string, array{label: string, actions: non-empty-list<Action>}>
|
||||
*/
|
||||
private const ACTION_GROUPS = [
|
||||
'downloads' => [
|
||||
'label' => 'All downloads',
|
||||
'actions' => self::DOWNLOAD_ACTIONS,
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ActivityPresenter $presenter,
|
||||
private readonly DownloadPresenter $downloadPresenter,
|
||||
@@ -41,6 +82,7 @@ class FileDetailsController extends Controller
|
||||
private readonly CommentingRules $commenting,
|
||||
private readonly FileVersionLinks $versionLinks,
|
||||
private readonly DownloadAllowance $allowance,
|
||||
private readonly TimezoneRegistry $timezones,
|
||||
) {}
|
||||
|
||||
public function show(Request $request, File $file): JsonResponse
|
||||
@@ -136,6 +178,65 @@ class FileDetailsController extends Controller
|
||||
return response()->json(['entries' => $entries, 'total' => $total]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Who has actually had this file: its downloads and previews, newest
|
||||
* first, with a count of each.
|
||||
*
|
||||
* A narrower question than activity() and a much more frequent one —
|
||||
* "did they ever actually get it?" — which the full log answers only
|
||||
* by being read past everything else that has happened to the file.
|
||||
*/
|
||||
public function access(Request $request, File $file): JsonResponse
|
||||
{
|
||||
$viewer = $request->user();
|
||||
assert($viewer !== null);
|
||||
Gate::forUser($viewer)->authorize('view', $file);
|
||||
abort_unless($viewer->can('view_actions_log'), 403);
|
||||
|
||||
$base = fn (): Builder => ActivityLog::query()
|
||||
->where('subject_type', $file->getMorphClass())
|
||||
->where('subject_id', $file->id);
|
||||
|
||||
$entries = $base()
|
||||
->whereIn('action', [...self::DOWNLOAD_ACTIONS, ...self::PREVIEW_ACTIONS])
|
||||
->orderByDesc('created_at')->orderByDesc('id')
|
||||
->limit(20)->get()
|
||||
->map(fn (ActivityLog $entry): array => [
|
||||
// The sentence the presenter builds already says which of
|
||||
// the two this was ("Downloaded the file …"), so nothing
|
||||
// here has to label the row a second time.
|
||||
...$this->presenter->present($entry),
|
||||
// Subject to the privacy setting that decides whether an
|
||||
// address is recorded at all, so it is often null.
|
||||
'ip_address' => $entry->ip_address,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'entries' => $entries,
|
||||
'downloads_total' => $base()->whereIn('action', self::DOWNLOAD_ACTIONS)->count(),
|
||||
'previews_total' => $base()->whereIn('action', self::PREVIEW_ACTIONS)->count(),
|
||||
// Built here rather than in the page: which filter value stands
|
||||
// for "every download" is a fact about the log's vocabulary,
|
||||
// and a group key only exists while the group does.
|
||||
'downloads_url' => $this->historyUrl($file, 'downloads', self::DOWNLOAD_ACTIONS),
|
||||
'previews_url' => $this->historyUrl($file, 'previews', self::PREVIEW_ACTIONS),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The file's history, pre-filtered to one question: by the group when
|
||||
* one covers these actions, and by the action itself when the group
|
||||
* would have a single member and therefore does not exist.
|
||||
*
|
||||
* @param non-empty-list<Action> $actions
|
||||
*/
|
||||
private function historyUrl(File $file, string $groupKey, array $actions): string
|
||||
{
|
||||
$filter = isset(self::ACTION_GROUPS[$groupKey]) ? $groupKey : $actions[0]->value;
|
||||
|
||||
return route('files.activity.history', $file, false).'?action='.$filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full, paginated activity history for a file — the "View full
|
||||
* history" destination linked from the details panel's Activity tab,
|
||||
@@ -148,7 +249,15 @@ class FileDetailsController extends Controller
|
||||
Gate::forUser($viewer)->authorize('view', $file);
|
||||
abort_unless($viewer->can('view_actions_log'), 403);
|
||||
|
||||
return $this->renderHistory($file->getMorphClass(), $file->id, $file->name, route('files.edit', $file, false));
|
||||
return $this->renderHistory(
|
||||
$request,
|
||||
$file->getMorphClass(),
|
||||
$file->id,
|
||||
$file->name,
|
||||
route('files.edit', $file, false).'?tab=activity',
|
||||
'files.activity.history',
|
||||
['file' => $file->id],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,7 +282,7 @@ class FileDetailsController extends Controller
|
||||
$query = ActivityLog::query()
|
||||
->where('subject_type', $file->getMorphClass())
|
||||
->where('subject_id', $file->id)
|
||||
->whereIn('action', [Action::FileDownloaded, Action::ShareLinkDownloaded, Action::PublicFileDownloaded]);
|
||||
->whereIn('action', self::DOWNLOAD_ACTIONS);
|
||||
|
||||
$total = (clone $query)->count();
|
||||
|
||||
@@ -304,15 +413,35 @@ class FileDetailsController extends Controller
|
||||
Gate::forUser($viewer)->authorize('view', $folder);
|
||||
abort_unless($viewer->can('view_actions_log'), 403);
|
||||
|
||||
return $this->renderHistory($folder->getMorphClass(), $folder->id, $folder->name, route('files.index', ['folder' => $folder->id], false));
|
||||
return $this->renderHistory(
|
||||
$request,
|
||||
$folder->getMorphClass(),
|
||||
$folder->id,
|
||||
$folder->name,
|
||||
route('files.index', ['folder' => $folder->id], false),
|
||||
'folders.activity.history',
|
||||
['folder' => $folder->id],
|
||||
);
|
||||
}
|
||||
|
||||
private function renderHistory(string $morphClass, int $subjectId, string $subjectName, string $backUrl): Response
|
||||
{
|
||||
$entries = ActivityLog::query()
|
||||
->where('subject_type', $morphClass)
|
||||
->where('subject_id', $subjectId)
|
||||
->orderByDesc('created_at')->orderByDesc('id')
|
||||
/**
|
||||
* @param array<string, mixed> $routeParams
|
||||
*/
|
||||
private function renderHistory(
|
||||
Request $request,
|
||||
string $morphClass,
|
||||
int $subjectId,
|
||||
string $subjectName,
|
||||
string $backUrl,
|
||||
string $routeName,
|
||||
array $routeParams,
|
||||
): Response {
|
||||
$viewer = $request->user();
|
||||
assert($viewer !== null);
|
||||
|
||||
$filters = $this->validatedHistoryFilters($request);
|
||||
|
||||
$entries = $this->historyQuery($morphClass, $subjectId, $filters, $viewer)
|
||||
->paginate(25)
|
||||
->withQueryString();
|
||||
|
||||
@@ -327,8 +456,135 @@ class FileDetailsController extends Controller
|
||||
'next' => $entries->nextPageUrl(),
|
||||
'total' => $entries->total(),
|
||||
],
|
||||
'filters' => $filters,
|
||||
'action_options' => $this->actionOptions($morphClass, $subjectId, $filters['action']),
|
||||
'subject_name' => $subjectName,
|
||||
'back_url' => $backUrl,
|
||||
'route_name' => $routeName,
|
||||
'route_params' => $routeParams,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The actions this subject's history actually contains, with how many
|
||||
* times each happened.
|
||||
*
|
||||
* Built from the log rather than from `Action::cases()`: the enum has
|
||||
* over eighty members and all but a handful can never appear against a
|
||||
* file, so offering them all would be a dropdown you scroll past the
|
||||
* answer in. What is here is what happened.
|
||||
*
|
||||
* @return list<array{key: string, label: string, count: int}>
|
||||
*/
|
||||
private function actionOptions(string $morphClass, int $subjectId, ?string $active): array
|
||||
{
|
||||
/** @var array<string, int> $counts */
|
||||
$counts = ActivityLog::query()
|
||||
->where('subject_type', $morphClass)
|
||||
->where('subject_id', $subjectId)
|
||||
->selectRaw('action, count(*) as total')
|
||||
->groupBy('action')
|
||||
->pluck('total', 'action')
|
||||
->map(fn ($total): int => (int) $total)
|
||||
->all();
|
||||
|
||||
$options = [];
|
||||
|
||||
foreach (self::ACTION_GROUPS as $key => $group) {
|
||||
$present = array_filter($group['actions'], fn (Action $action): bool => isset($counts[$action->value]));
|
||||
|
||||
// One member present means the group would filter to exactly
|
||||
// what its member already offers, under a vaguer name — unless
|
||||
// this *is* what is currently being filtered on (the file
|
||||
// page's "View all downloads" button links straight to it), in
|
||||
// which case the dropdown has to be able to show its own value.
|
||||
if (count($present) < 2 && $active !== $key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[] = [
|
||||
'key' => $key,
|
||||
'label' => $group['label'],
|
||||
'count' => array_sum(array_map(fn (Action $action): int => $counts[$action->value], $present)),
|
||||
];
|
||||
}
|
||||
|
||||
// Enum order, not count order, so the list does not rearrange
|
||||
// itself under the reader every time the file is downloaded.
|
||||
foreach (Action::cases() as $action) {
|
||||
// Same reason as the group above: a filter arrived at from a
|
||||
// link stays visible in the dropdown even at a count of zero,
|
||||
// rather than leaving it blank over an empty table.
|
||||
if (! isset($counts[$action->value]) && $active !== $action->value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[] = [
|
||||
'key' => $action->value,
|
||||
'label' => $action->description(),
|
||||
'count' => $counts[$action->value] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{action: ?string, actor: ?string, from: ?string, to: ?string}
|
||||
*/
|
||||
private function validatedHistoryFilters(Request $request): array
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'action' => ['nullable', Rule::in([
|
||||
...array_keys(self::ACTION_GROUPS),
|
||||
...array_column(Action::cases(), 'value'),
|
||||
])],
|
||||
'actor' => ['nullable', 'string', 'max:255'],
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date', 'after_or_equal:from'],
|
||||
]);
|
||||
|
||||
return [
|
||||
'action' => $validated['action'] ?? null,
|
||||
'actor' => $validated['actor'] ?? null,
|
||||
'from' => $validated['from'] ?? null,
|
||||
'to' => $validated['to'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{action: ?string, actor: ?string, from: ?string, to: ?string} $filters
|
||||
* @return Builder<ActivityLog>
|
||||
*/
|
||||
private function historyQuery(string $morphClass, int $subjectId, array $filters, User $viewer): Builder
|
||||
{
|
||||
$timezone = $this->timezones->resolve($viewer);
|
||||
|
||||
return ActivityLog::query()
|
||||
->where('subject_type', $morphClass)
|
||||
->where('subject_id', $subjectId)
|
||||
->when($filters['action'], function (Builder $query, string $action): void {
|
||||
$group = self::ACTION_GROUPS[$action] ?? null;
|
||||
|
||||
$group === null
|
||||
? $query->where('action', $action)
|
||||
: $query->whereIn('action', array_map(fn (Action $member): string => $member->value, $group['actions']));
|
||||
})
|
||||
// Matched on the name snapshotted onto the entry, the same as
|
||||
// the main log: an account deleted since is still findable by
|
||||
// the name it acted under, which is the whole point of the
|
||||
// snapshot.
|
||||
->when($filters['actor'], fn (Builder $query, string $actor) => $query->where('actor_name', 'like', "%{$actor}%"))
|
||||
// The reader's own calendar day, not the UTC one — see LocalDay.
|
||||
->when(
|
||||
$filters['from'] !== null ? LocalDay::start($filters['from'], $timezone) : null,
|
||||
fn (Builder $query, Carbon $from) => $query->where('created_at', '>=', $from),
|
||||
)
|
||||
->when(
|
||||
$filters['to'] !== null ? LocalDay::end($filters['to'], $timezone) : null,
|
||||
fn (Builder $query, Carbon $to) => $query->where('created_at', '<=', $to),
|
||||
)
|
||||
->orderByDesc('created_at')
|
||||
->orderByDesc('id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,28 +8,26 @@ use App\Http\Controllers\Controller;
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Files\Access\DownloadAllowance;
|
||||
use App\Modules\Files\Delivery\StoredFileResponse;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Support\ContentDisposition;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Authorized downloads without the bytes ever traversing PHP: for a file
|
||||
* on the local disk, the app checks the policy and answers with
|
||||
* X-Accel-Redirect; nginx streams the file from the protected location
|
||||
* (brief §3). The cloud edition swaps this for presigned URLs behind the
|
||||
* same route. A file on the community-only external storage disk already
|
||||
* gets exactly that — a presigned URL redirect — since nginx has no way
|
||||
* to serve bytes it doesn't have on disk.
|
||||
* Authorized downloads without the bytes ever traversing PHP: the app
|
||||
* checks the policy, and StoredFileResponse answers with either an
|
||||
* X-Accel-Redirect for nginx to stream from the protected location
|
||||
* (brief §3) or a presigned URL when the file lives on external storage,
|
||||
* since nginx has no way to serve bytes it doesn't have on disk.
|
||||
*/
|
||||
class FileDownloadController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ActivityLogger $activity,
|
||||
private readonly DownloadAllowance $allowance,
|
||||
private readonly StoredFileResponse $bytes,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request, File $file): Response|RedirectResponse
|
||||
@@ -44,21 +42,6 @@ class FileDownloadController extends Controller
|
||||
|
||||
$this->activity->log(Action::FileDownloaded, subject: $file);
|
||||
|
||||
if ($file->disk !== 'files') {
|
||||
$url = Storage::disk($file->disk)->temporaryUrl(
|
||||
$file->path,
|
||||
now()->addHour(),
|
||||
['ResponseContentDisposition' => ContentDisposition::attachment($file->original_name)],
|
||||
);
|
||||
|
||||
return redirect()->away($url);
|
||||
}
|
||||
|
||||
return response('', 200, [
|
||||
'X-Accel-Redirect' => '/protected-files/'.$file->path,
|
||||
'Content-Type' => $file->mime_type,
|
||||
'Content-Disposition' => ContentDisposition::attachment($file->original_name),
|
||||
'Content-Length' => (string) $file->size,
|
||||
]);
|
||||
return $this->bytes->attachment($file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,21 @@ use App\Http\Controllers\Controller;
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Files\Access\DownloadAllowance;
|
||||
use App\Modules\Files\Delivery\StoredFileResponse;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Modules\Files\Preview\PreviewKind;
|
||||
use App\Modules\Files\Thumbnails\Events\ResolvingImageRendering;
|
||||
use App\Modules\Files\Thumbnails\ImageAudience;
|
||||
use App\Modules\Files\Thumbnails\ImageRendition;
|
||||
use App\Modules\Files\Thumbnails\LocalSourceFile;
|
||||
use App\Modules\Files\Thumbnails\ThumbnailGenerator;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use App\Support\ContentDisposition;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -32,17 +38,25 @@ use Illuminate\Support\Facades\Storage;
|
||||
* file's contents — a real, audit-worthy action, just not a "download."
|
||||
*
|
||||
* SECURITY: both methods serve bytes inline, from this app's own origin,
|
||||
* with the File's stored mime type — so both are restricted to
|
||||
* with the File's stored mime type, so both are restricted to an
|
||||
* allowlist — but not the same one, because they are asking different
|
||||
* questions. `thumbnail()` is bounded by
|
||||
* ThumbnailGenerator::SUPPORTED_MIME_TYPES, the raster formats this app
|
||||
* renders itself. That list is the allowlist; nothing else is ever served
|
||||
* inline. Do NOT widen it to text/html, image/svg+xml, or anything else a
|
||||
* browser executes script from, and do not reach for the upload
|
||||
* allowed-extensions setting as a substitute: that setting matches on the
|
||||
* *extension*, while mime_type is detected from the *bytes*
|
||||
* (ChunkedUploadsController::complete), so a .txt holding HTML is stored
|
||||
* as text/html and would render as a page here. Serving a file inline as
|
||||
* a type the browser executes is same-origin script execution with the
|
||||
* viewer's session.
|
||||
* decodes and re-encodes itself, since a thumbnail *is* a rendition.
|
||||
* `preview()` is bounded by PreviewKind, which additionally admits the
|
||||
* video, audio and PDF types a browser plays natively and this app never
|
||||
* touches. PreviewKind's docblock carries the rule in full; the short
|
||||
* version is that neither list may ever grow a type a browser executes
|
||||
* script from, and neither may be derived from the upload
|
||||
* allowed-extensions setting, which matches on the *extension* while
|
||||
* mime_type is detected from the *bytes*
|
||||
* (ChunkedUploadsController::complete).
|
||||
*
|
||||
* Serving media inline is also why `preview()` logs at most one
|
||||
* Action::FilePreviewed per viewer per file per five minutes: a `<video>`
|
||||
* seeking through a recording issues a long tail of Range requests
|
||||
* against this same URL, and one row each would bury the log under a
|
||||
* single deliberate act.
|
||||
*
|
||||
* Renditions always cache on the local "files" disk regardless of where
|
||||
* the source file lives — they're a derived artifact, not the original,
|
||||
@@ -60,6 +74,9 @@ class FileThumbnailController extends Controller
|
||||
private readonly ThumbnailGenerator $thumbnails,
|
||||
private readonly ActivityLogger $activity,
|
||||
private readonly DownloadAllowance $allowance,
|
||||
private readonly StoredFileResponse $bytes,
|
||||
private readonly LocalSourceFile $source,
|
||||
private readonly Settings $settings,
|
||||
) {}
|
||||
|
||||
public function thumbnail(Request $request, File $file): Response
|
||||
@@ -82,66 +99,92 @@ class FileThumbnailController extends Controller
|
||||
/**
|
||||
* A file opened to be looked at.
|
||||
*
|
||||
* A preview is not the file — it is a rendered view of it, which is
|
||||
* why it may be decorated at all. But rendering one is expensive
|
||||
* (decoding and re-encoding a full-size photograph) where serving the
|
||||
* stored bytes is nearly free, so core only pays that cost when a
|
||||
* listener says this particular viewer must be served a rendering:
|
||||
* ResolvingImageRendering asks, and defaults to no. On an
|
||||
* For an image, a preview is not the file — it is a rendered view of
|
||||
* it, which is why it may be decorated at all. But rendering one is
|
||||
* expensive (decoding and re-encoding a full-size photograph) where
|
||||
* serving the stored bytes is nearly free, so core only pays that
|
||||
* cost when a listener says this particular viewer must be served a
|
||||
* rendering: ResolvingImageRendering asks, and defaults to no. On an
|
||||
* installation that watermarks, a client gets a bounded, watermarked
|
||||
* render and staff get the original; on one that does not, everyone
|
||||
* gets exactly what this endpoint has always returned.
|
||||
*
|
||||
* For video, audio and PDF there is no rendering to resolve — this
|
||||
* app cannot decode any of them, so it has no rendition to cache, no
|
||||
* watermark to stamp, and nothing to ask about. Those go straight to
|
||||
* the bytes.
|
||||
*/
|
||||
public function preview(Request $request, File $file): Response|RedirectResponse
|
||||
{
|
||||
Gate::authorize('view', $file);
|
||||
|
||||
// Only types this app renders itself may be served inline; anything
|
||||
// else is a download, not a preview. See the class docblock — the
|
||||
// stored mime type is sniffed from the bytes, so an allowed
|
||||
// The inline allowlist. See the class docblock and PreviewKind —
|
||||
// the stored mime type is sniffed from the bytes, so an allowed
|
||||
// extension is not evidence of a safe-to-render payload.
|
||||
abort_unless(ThumbnailGenerator::supports($file->mime_type), 404);
|
||||
$kind = PreviewKind::forMime($file->mime_type);
|
||||
|
||||
abort_if($kind === null, 404);
|
||||
|
||||
// Staff are never gated: this switch exists so an installation can
|
||||
// decide what its *clients* may do with a file short of taking it.
|
||||
// 404 rather than 403 because with the setting off the endpoint is
|
||||
// not a thing that exists for this viewer.
|
||||
abort_if(
|
||||
$request->user()?->isStaff() !== true && ! $this->settings->get(Setting::ClientsCanPreviewFiles),
|
||||
404,
|
||||
);
|
||||
|
||||
// A preview is not counted as a download, but it is refused once
|
||||
// the download limit is spent — because unless a listener asks
|
||||
// for a rendering (nothing does by default), the branches below
|
||||
// serve the *original bytes* at full size. Without this a cap
|
||||
// would be one URL away from meaningless for every image on the
|
||||
// install. thumbnail() needs no such guard: a 300px rendition is
|
||||
// not the file.
|
||||
// for a rendering (nothing does by default, and nothing ever does
|
||||
// for media), the branches below serve the *original bytes* at
|
||||
// full size. Without this a cap would be one URL away from
|
||||
// meaningless for every previewable file on the install.
|
||||
// thumbnail() needs no such guard: a 300px rendition is not the
|
||||
// file.
|
||||
abort_unless($this->allowance->allows($file, $request->user()), 403);
|
||||
|
||||
$this->activity->log(Action::FilePreviewed, subject: $file);
|
||||
$this->logPreview($file, $request);
|
||||
|
||||
$audience = ImageAudience::forViewer($request->user());
|
||||
if ($kind === PreviewKind::Image) {
|
||||
$audience = ImageAudience::forViewer($request->user());
|
||||
|
||||
$decision = new ResolvingImageRendering($audience, ImageRendition::Preview, $file->mime_type);
|
||||
Event::dispatch($decision);
|
||||
$decision = new ResolvingImageRendering($audience, ImageRendition::Preview, $file->mime_type);
|
||||
Event::dispatch($decision);
|
||||
|
||||
if ($decision->required) {
|
||||
$path = $this->render($file, $audience, ImageRendition::Preview);
|
||||
if ($decision->required) {
|
||||
$path = $this->render($file, $audience, ImageRendition::Preview);
|
||||
|
||||
abort_if($path === null, 404);
|
||||
abort_if($path === null, 404);
|
||||
|
||||
return $this->serve($file, $path);
|
||||
return $this->serve($file, $path);
|
||||
}
|
||||
}
|
||||
|
||||
if ($file->disk !== 'files') {
|
||||
$url = Storage::disk($file->disk)->temporaryUrl(
|
||||
$file->path,
|
||||
now()->addHour(),
|
||||
['ResponseContentDisposition' => ContentDisposition::inline($file->original_name)],
|
||||
);
|
||||
return $this->bytes->inline($file);
|
||||
}
|
||||
|
||||
return redirect()->away($url);
|
||||
/**
|
||||
* One log row per viewer per file per five minutes.
|
||||
*
|
||||
* Watching a video is a single deliberate act that the browser turns
|
||||
* into dozens of Range requests against this route, and each one
|
||||
* arrives here indistinguishable from someone clicking preview again.
|
||||
* Cache::add is the whole mechanism: it writes only if the key is
|
||||
* absent, so the first request through the window logs and the rest
|
||||
* are silent, without a read-then-write race between two of them.
|
||||
*
|
||||
* Keyed by viewer, so one client's playback never suppresses another
|
||||
* person's preview of the same file. Anonymous viewers do not reach
|
||||
* this route at all — see PublicGroupsController::preview.
|
||||
*/
|
||||
private function logPreview(File $file, Request $request): void
|
||||
{
|
||||
$key = 'file-preview-logged:'.$file->id.':'.($request->user()->id ?? 'guest');
|
||||
|
||||
if (Cache::add($key, true, now()->addMinutes(5))) {
|
||||
$this->activity->log(Action::FilePreviewed, subject: $file);
|
||||
}
|
||||
|
||||
return response('', 200, [
|
||||
'X-Accel-Redirect' => '/protected-files/'.$file->path,
|
||||
'Content-Type' => $file->mime_type,
|
||||
'Content-Disposition' => ContentDisposition::inline($file->original_name),
|
||||
'Content-Length' => (string) $file->size,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,15 +207,14 @@ class FileThumbnailController extends Controller
|
||||
}
|
||||
|
||||
$disk->makeDirectory(dirname($path));
|
||||
$sourcePath = $this->localSourcePathFor($file);
|
||||
|
||||
try {
|
||||
$this->thumbnails->generate($sourcePath, $disk->path($path), $file->mime_type, $audience, $rendition);
|
||||
} finally {
|
||||
if ($file->disk !== 'files') {
|
||||
@unlink($sourcePath);
|
||||
}
|
||||
}
|
||||
$this->source->use($file, fn (string $sourcePath) => $this->thumbnails->generate(
|
||||
$sourcePath,
|
||||
$disk->path($path),
|
||||
$file->mime_type,
|
||||
$audience,
|
||||
$rendition,
|
||||
));
|
||||
|
||||
return $path;
|
||||
}
|
||||
@@ -185,38 +227,4 @@ class FileThumbnailController extends Controller
|
||||
'Content-Disposition' => ContentDisposition::inline($file->original_name),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A local-disk file's real path (fast path). Anything else is
|
||||
* stream-copied to a temp file first — the caller unlinks it once
|
||||
* rendering is done.
|
||||
*/
|
||||
private function localSourcePathFor(File $file): string
|
||||
{
|
||||
if ($file->disk === 'files') {
|
||||
return Storage::disk('files')->path($file->path);
|
||||
}
|
||||
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'thumb-src-');
|
||||
|
||||
if ($tempPath === false) {
|
||||
throw new \RuntimeException('Could not create a temp file for '.$file->original_name);
|
||||
}
|
||||
|
||||
$stream = Storage::disk($file->disk)->readStream($file->path);
|
||||
$out = fopen($tempPath, 'wb');
|
||||
|
||||
if ($stream === null || $out === false) {
|
||||
throw new \RuntimeException('Could not read '.$file->original_name.' from its storage disk.');
|
||||
}
|
||||
|
||||
stream_copy_to_stream($stream, $out);
|
||||
fclose($out);
|
||||
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
return $tempPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,11 @@ class FilesController extends Controller
|
||||
'can_update' => Gate::forUser($viewer)->allows('update', $file),
|
||||
'can_delete' => Gate::forUser($viewer)->allows('delete', $file),
|
||||
'can_manage_public' => $viewer->can('upload_public'),
|
||||
// Whether this page offers its Activity tab. The file's own
|
||||
// page is where somebody lands from a link, a search or a
|
||||
// notification, so "what happened to this file" has to be
|
||||
// answerable here and not only from the library's list.
|
||||
'can_view_activity' => $viewer->can('view_actions_log'),
|
||||
// The per-file switch only does anything while the comment
|
||||
// scope is `selected`; under every other value the page hides
|
||||
// it rather than offer a control with no current effect.
|
||||
|
||||
@@ -260,6 +260,13 @@ class MyFilesController extends Controller
|
||||
'can_upload' => $client->can('upload'),
|
||||
'can_upload_here' => Folder::uploadableBy($client, $current),
|
||||
'can_create_folders' => $client->can('create_own_folders'),
|
||||
// Whether a row is clickable to look at rather than only to
|
||||
// take. Per page rather than per file: the mime type decides
|
||||
// which files can be previewed and every theme already knows
|
||||
// how to read one, so all this has to carry is whether the
|
||||
// installation offers it here at all. See
|
||||
// FileThumbnailController::preview, which re-checks it.
|
||||
'preview_enabled' => $this->settings->get(Setting::ClientsCanPreviewFiles),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ use App\Http\Controllers\Controller;
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Files\Access\DownloadAllowance;
|
||||
use App\Modules\Files\Delivery\StoredFileResponse;
|
||||
use App\Modules\Files\Models\Category;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Modules\Files\Models\ShareLink;
|
||||
use App\Support\ContentDisposition;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Inertia\Inertia;
|
||||
@@ -28,6 +28,7 @@ class PublicShareController extends Controller
|
||||
public function __construct(
|
||||
private readonly ActivityLogger $activity,
|
||||
private readonly DownloadAllowance $allowance,
|
||||
private readonly StoredFileResponse $bytes,
|
||||
) {}
|
||||
|
||||
public function show(string $token): InertiaResponse
|
||||
@@ -101,11 +102,6 @@ class PublicShareController extends Controller
|
||||
|
||||
$this->activity->log(Action::ShareLinkDownloaded, subject: $file);
|
||||
|
||||
return response('', 200, [
|
||||
'X-Accel-Redirect' => '/protected-files/'.$file->path,
|
||||
'Content-Type' => $file->mime_type,
|
||||
'Content-Disposition' => ContentDisposition::attachment($file->original_name),
|
||||
'Content-Length' => (string) $file->size,
|
||||
]);
|
||||
return $this->bytes->attachment($file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Files\Preview;
|
||||
|
||||
use App\Modules\Files\Thumbnails\ThumbnailGenerator;
|
||||
|
||||
/**
|
||||
* What kind of inline view, if any, a stored file gets — the single
|
||||
* answer to "may these bytes be served inline, and what element renders
|
||||
* them?", shared by FileThumbnailController::preview (signed in) and
|
||||
* PublicGroupsController::preview (anonymous).
|
||||
*
|
||||
* SECURITY: this is an allowlist, and it is the boundary. Preview serves
|
||||
* a file's own bytes inline, from this app's origin, labelled with the
|
||||
* mime type stored on the row — so anything a browser executes script
|
||||
* from would be same-origin script execution with the viewer's session.
|
||||
* Never add text/html, image/svg+xml, or any other document type, and
|
||||
* never derive this list from Setting::AllowedUploadExtensions: that
|
||||
* setting matches on the *extension* while mime_type is sniffed from the
|
||||
* *bytes* (ChunkedUploadsController::complete), so a .txt holding HTML is
|
||||
* stored as text/html and would arrive here looking allowed.
|
||||
*
|
||||
* Deliberately narrower than "files a browser might cope with": every
|
||||
* type below is one every current browser decodes natively. Formats like
|
||||
* video/quicktime, video/x-msvideo and video/x-matroska are left out
|
||||
* because an embedded player for them shows a black rectangle. They
|
||||
* upload and download exactly as before — only the inline view is
|
||||
* withheld.
|
||||
*
|
||||
* Distinct from ThumbnailGenerator::SUPPORTED_MIME_TYPES, which answers a
|
||||
* narrower question: which types this app can *decode and re-encode*
|
||||
* itself, and therefore has renditions, a cache and a watermark hook for.
|
||||
* Image delegates to it rather than restating it, so the two cannot drift.
|
||||
*/
|
||||
enum PreviewKind: string
|
||||
{
|
||||
/** Rendered with <img>; the only kind with thumbnails and renditions. */
|
||||
case Image = 'image';
|
||||
|
||||
/** Rendered with <video controls>. */
|
||||
case Video = 'video';
|
||||
|
||||
/** Rendered with <audio controls>. */
|
||||
case Audio = 'audio';
|
||||
|
||||
/** Rendered in a sandboxed <iframe>, by the browser's own viewer. */
|
||||
case Pdf = 'pdf';
|
||||
|
||||
/** @var list<string> */
|
||||
private const VIDEO_MIME_TYPES = [
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
];
|
||||
|
||||
/**
|
||||
* More spellings than there are formats: the mime type is whatever
|
||||
* finfo made of the bytes, and it is not consistent across systems —
|
||||
* a .wav is audio/x-wav on one box and audio/vnd.wave on another, and
|
||||
* an .m4a can come back as audio/mp4 or audio/x-m4a.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const AUDIO_MIME_TYPES = [
|
||||
'audio/mpeg',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/vnd.wave',
|
||||
'audio/ogg',
|
||||
'audio/webm',
|
||||
'audio/mp4',
|
||||
'audio/x-m4a',
|
||||
'audio/aac',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
|
||||
public static function forMime(string $mimeType): ?self
|
||||
{
|
||||
if (ThumbnailGenerator::supports($mimeType)) {
|
||||
return self::Image;
|
||||
}
|
||||
|
||||
if (in_array($mimeType, self::VIDEO_MIME_TYPES, true)) {
|
||||
return self::Video;
|
||||
}
|
||||
|
||||
if (in_array($mimeType, self::AUDIO_MIME_TYPES, true)) {
|
||||
return self::Audio;
|
||||
}
|
||||
|
||||
if ($mimeType === 'application/pdf') {
|
||||
return self::Pdf;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function supports(string $mimeType): bool
|
||||
{
|
||||
return self::forMime($mimeType) !== null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Files\Thumbnails;
|
||||
|
||||
use App\Modules\Files\Models\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A real path on this machine for a stored file, so that something which
|
||||
* can only work on local bytes — image and video rendering, all of which
|
||||
* shells out or hands a path to a C library — can work on any file
|
||||
* whatever disk it lives on.
|
||||
*
|
||||
* A local file is used where it lies. Anything else is stream-copied to a
|
||||
* temp file and removed afterwards.
|
||||
*
|
||||
* The callback shape is the point. This started as a private method on
|
||||
* one controller that returned a path and left the caller to unlink it,
|
||||
* and the second place that needed it did not call it at all — it passed
|
||||
* the *local* disk's path() for a file on external storage, which is a
|
||||
* path that does not exist, so every public-listing thumbnail of an
|
||||
* externally stored file failed. Handing back a path is an invitation to
|
||||
* both of those mistakes; a closure that owns the lifetime is not.
|
||||
*/
|
||||
class LocalSourceFile
|
||||
{
|
||||
/**
|
||||
* @template TReturn
|
||||
*
|
||||
* @param callable(string): TReturn $work
|
||||
* @return TReturn
|
||||
*/
|
||||
public function use(File $file, callable $work): mixed
|
||||
{
|
||||
if ($file->disk === 'files') {
|
||||
return $work(Storage::disk('files')->path($file->path));
|
||||
}
|
||||
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'thumb-src-');
|
||||
|
||||
if ($tempPath === false) {
|
||||
throw new RuntimeException('Could not create a temp file for '.$file->original_name);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->copyDown($file, $tempPath);
|
||||
|
||||
return $work($tempPath);
|
||||
} finally {
|
||||
@unlink($tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
private function copyDown(File $file, string $tempPath): void
|
||||
{
|
||||
$stream = Storage::disk($file->disk)->readStream($file->path);
|
||||
$out = fopen($tempPath, 'wb');
|
||||
|
||||
if ($stream === null || $out === false) {
|
||||
if (is_resource($out)) {
|
||||
fclose($out);
|
||||
}
|
||||
|
||||
throw new RuntimeException('Could not read '.$file->original_name.' from its storage disk.');
|
||||
}
|
||||
|
||||
try {
|
||||
stream_copy_to_stream($stream, $out);
|
||||
} finally {
|
||||
fclose($out);
|
||||
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace App\Modules\Files\Uploads;
|
||||
use App\Modules\Files\Storage\ResolvingUploadDisk;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\File as FileSystem;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use RuntimeException;
|
||||
@@ -203,12 +204,40 @@ class LocalPartStore
|
||||
Event::dispatch($diskEvent);
|
||||
$disk = $diskEvent->disk;
|
||||
|
||||
Storage::disk($disk)->writeStream($targetPath, $readStream);
|
||||
$written = Storage::disk($disk)->writeStream($targetPath, $readStream);
|
||||
|
||||
if (is_resource($readStream)) {
|
||||
fclose($readStream);
|
||||
}
|
||||
|
||||
// The disks are configured with 'throw' => false, so a refused
|
||||
// write is a `false` return rather than an exception — and the
|
||||
// caller goes on to record a File row for bytes that were never
|
||||
// stored. Losing an upload silently is worse than failing it, and
|
||||
// this is the only place that can tell the difference: a real
|
||||
// instance of it was a GCS bucket rejecting the adapter's ACL,
|
||||
// which looked exactly like a successful upload.
|
||||
if ($written === false) {
|
||||
// The reason is lost by the time it gets here — 'throw' => false
|
||||
// means Flysystem swallowed the exception rather than passing it
|
||||
// on — so log what was attempted. Which bucket it was is the
|
||||
// difference between reading this as "my credentials expired"
|
||||
// and "I typed the wrong bucket name", and only the log can say
|
||||
// it: the message below is shown to whoever was uploading, which
|
||||
// includes clients, and a bucket name is not theirs to see.
|
||||
Log::error('Upload could not be written to storage.', [
|
||||
'disk' => $disk,
|
||||
'bucket' => config('filesystems.disks.'.$disk.'.bucket'),
|
||||
'driver' => config('filesystems.disks.'.$disk.'.driver'),
|
||||
'path' => $targetPath,
|
||||
]);
|
||||
|
||||
throw new RuntimeException(
|
||||
'Could not write the assembled upload to the "'.$disk.'" disk. '
|
||||
.'Check the storage backend is reachable and its credentials are still valid.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->abort($session);
|
||||
|
||||
return [
|
||||
|
||||
@@ -9,11 +9,14 @@ use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Comments\CommentingRules;
|
||||
use App\Modules\Files\Access\DownloadAllowance;
|
||||
use App\Modules\Files\Delivery\StoredFileResponse;
|
||||
use App\Modules\Files\Models\Category;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Modules\Files\Models\Folder;
|
||||
use App\Modules\Files\Preview\PreviewKind;
|
||||
use App\Modules\Files\Thumbnails\ImageAudience;
|
||||
use App\Modules\Files\Thumbnails\ImageRendition;
|
||||
use App\Modules\Files\Thumbnails\LocalSourceFile;
|
||||
use App\Modules\Files\Thumbnails\ThumbnailGenerator;
|
||||
use App\Modules\Files\Versions\FileVersionLinks;
|
||||
use App\Modules\Groups\Http\Controllers\Concerns\InteractsWithPublicListing;
|
||||
@@ -79,6 +82,8 @@ class PublicGroupsController extends Controller
|
||||
private readonly PublicThemeRegistry $themes,
|
||||
private readonly CapabilityRegistry $capabilities,
|
||||
private readonly CommentingRules $commenting,
|
||||
private readonly StoredFileResponse $bytes,
|
||||
private readonly LocalSourceFile $source,
|
||||
) {}
|
||||
|
||||
public function index(Request $request, string $publicSlug): InertiaResponse|RedirectResponse
|
||||
@@ -208,6 +213,13 @@ class PublicGroupsController extends Controller
|
||||
'thumbnail_url' => ThumbnailGenerator::supports($file->mime_type)
|
||||
? route('public.thumbnail', [$publicSlug, $file->slug])
|
||||
: null,
|
||||
// Null whenever preview is unavailable, for any of the three
|
||||
// reasons — switched off, wrong type, or the download limit
|
||||
// spent — so a theme has one thing to check and the setting
|
||||
// itself never ships to a visitor's browser. preview() below
|
||||
// re-checks all three: this decides what to offer, not what
|
||||
// is allowed.
|
||||
'preview_url' => $this->previewUrlFor($file, $publicSlug),
|
||||
'download_url' => route('public.download', [$publicSlug, $file->slug]),
|
||||
// Same decided shape the listings send, so a theme's single
|
||||
// file page disables its button for the same reason a row
|
||||
@@ -241,7 +253,18 @@ class PublicGroupsController extends Controller
|
||||
|
||||
if (! $disk->exists($thumbnailPath)) {
|
||||
$disk->makeDirectory(dirname($thumbnailPath));
|
||||
$this->thumbnails->generate($disk->path($file->path), $disk->path($thumbnailPath), $file->mime_type, ImageAudience::External, ImageRendition::Thumbnail);
|
||||
|
||||
// Never $disk->path($file->path): the rendition is cached on
|
||||
// the local disk, but the *source* lives on whichever disk the
|
||||
// file was uploaded to, and a local path for an externally
|
||||
// stored file is a path that does not exist.
|
||||
$this->source->use($file, fn (string $sourcePath) => $this->thumbnails->generate(
|
||||
$sourcePath,
|
||||
$disk->path($thumbnailPath),
|
||||
$file->mime_type,
|
||||
ImageAudience::External,
|
||||
ImageRendition::Thumbnail,
|
||||
));
|
||||
}
|
||||
|
||||
return response('', 200, [
|
||||
@@ -251,6 +274,58 @@ class PublicGroupsController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The anonymous twin of FileThumbnailController::preview: a public
|
||||
* file shown rather than handed over.
|
||||
*
|
||||
* Nothing is rendered or cached here — an anonymous viewer only ever
|
||||
* previews the stored bytes. The watermark hook that decorates a
|
||||
* client's image preview has no equivalent on this route, for the
|
||||
* same reason thumbnail() hardcodes ImageAudience::External: there is
|
||||
* no viewer to tell apart.
|
||||
*/
|
||||
public function preview(string $publicSlug, File $file): Response|RedirectResponse
|
||||
{
|
||||
$this->guardSlug($publicSlug);
|
||||
|
||||
abort_unless($file->isEffectivelyPublic() && ! $file->isExpired(), 404);
|
||||
abort_unless($this->settings->get(Setting::PublicListingPreviewEnabled) === true, 404);
|
||||
abort_if(PreviewKind::forMime($file->mime_type) === null, 404);
|
||||
|
||||
// 403 rather than 404 for the same reason download() does it, and
|
||||
// it is the same allowance being read: preview serves the whole
|
||||
// file, so a spent cap has to close this door too or it closes
|
||||
// nothing.
|
||||
abort_unless($this->allowance->allows($file, null), 403);
|
||||
|
||||
$this->activity->log(Action::PublicFilePreviewed, subject: $file);
|
||||
|
||||
return $this->bytes->inline($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* What showFile() offers, which is not the same question as what
|
||||
* preview() permits — this one also declines to advertise a preview
|
||||
* whose download limit is already spent, so a visitor is not given a
|
||||
* button that can only answer 403.
|
||||
*/
|
||||
private function previewUrlFor(File $file, string $publicSlug): ?string
|
||||
{
|
||||
if ($this->settings->get(Setting::PublicListingPreviewEnabled) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (PreviewKind::forMime($file->mime_type) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $this->allowance->allows($file, null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return route('public.preview', [$publicSlug, $file->slug]);
|
||||
}
|
||||
|
||||
public function download(string $publicSlug, File $file): Response
|
||||
{
|
||||
$this->guardSlug($publicSlug);
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Identity\Models\Role;
|
||||
use App\Modules\Identity\Permissions\SystemRole;
|
||||
use App\Modules\Identity\UserType;
|
||||
use App\Modules\Platform\Onboarding\InstallationWelcome;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Console\Command;
|
||||
@@ -72,6 +73,12 @@ class CreateAdminCommand extends Command
|
||||
$settings->set(Setting::AdminNotificationEmails, [$user->email]);
|
||||
}
|
||||
|
||||
// Unattended provisioning skips the setup screen entirely, so this
|
||||
// is the only place that can record "somebody just installed this"
|
||||
// for a container that came up from environment variables. They
|
||||
// still deserve showing around on their first visit.
|
||||
app(InstallationWelcome::class)->raise();
|
||||
|
||||
$this->info("Administrator {$user->email} created.");
|
||||
|
||||
return self::SUCCESS;
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Identity\Models\Role;
|
||||
use App\Modules\Identity\Permissions\SystemRole;
|
||||
use App\Modules\Identity\UserType;
|
||||
use App\Modules\Platform\Onboarding\InstallationWelcome;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -28,6 +29,7 @@ class SetupController extends Controller
|
||||
public function __construct(
|
||||
private readonly Settings $settings,
|
||||
private readonly ActivityLogger $activity,
|
||||
private readonly InstallationWelcome $welcome,
|
||||
) {}
|
||||
|
||||
public function show(): Response|RedirectResponse
|
||||
@@ -72,6 +74,11 @@ class SetupController extends Controller
|
||||
$this->settings->set(Setting::AdminNotificationEmails, [$admin->email]);
|
||||
}
|
||||
|
||||
// They will be shown around the first time they sign in — which is
|
||||
// the next thing that happens, since setup deliberately does not
|
||||
// log anybody in.
|
||||
$this->welcome->raise();
|
||||
|
||||
// Deliberately no auto-login: the new administrator proves their
|
||||
// credentials at the login form, which also confirms they work.
|
||||
return redirect()->route('setup.success')->with('setup_completed', true);
|
||||
|
||||
@@ -15,7 +15,8 @@ use App\Modules\Identity\Social\SocialProvider;
|
||||
use App\Modules\Identity\Social\SocialSettings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
@@ -57,13 +58,13 @@ class SocialLoginController extends Controller
|
||||
}
|
||||
|
||||
/** Begin a sign-in. */
|
||||
public function redirect(Request $request, string $provider): SymfonyRedirectResponse|RedirectResponse
|
||||
public function redirect(Request $request, string $provider): Response
|
||||
{
|
||||
return $this->begin($request, $provider, 'login');
|
||||
}
|
||||
|
||||
/** Begin connecting a provider to the signed-in account. */
|
||||
public function connect(Request $request, string $provider): SymfonyRedirectResponse|RedirectResponse
|
||||
public function connect(Request $request, string $provider): Response
|
||||
{
|
||||
return $this->begin($request, $provider, 'link');
|
||||
}
|
||||
@@ -130,7 +131,7 @@ class SocialLoginController extends Controller
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
private function begin(Request $request, string $provider, string $intent): SymfonyRedirectResponse|RedirectResponse
|
||||
private function begin(Request $request, string $provider, string $intent): Response
|
||||
{
|
||||
$case = $this->provider($provider);
|
||||
$settings = SocialSettings::for($case);
|
||||
@@ -142,7 +143,13 @@ class SocialLoginController extends Controller
|
||||
|
||||
$request->session()->put([self::INTENT => $intent, self::PROVIDER => $case->value]);
|
||||
|
||||
return $this->gateway()->redirect($settings);
|
||||
// Inertia::location(), not the redirect itself. Connecting starts
|
||||
// as an Inertia XHR from the settings screen, and an XHR follows a
|
||||
// 302 to the provider cross-origin, where CORS kills it before the
|
||||
// person ever leaves the page. The 409 + X-Inertia-Location pair
|
||||
// makes the client navigate top-level instead; a plain browser
|
||||
// request — the login flow — passes through unchanged.
|
||||
return Inertia::location($this->gateway()->redirect($settings));
|
||||
}
|
||||
|
||||
private function completeLink(Request $request, SocialSettings $settings, SocialIdentity $identity): RedirectResponse
|
||||
|
||||
@@ -138,6 +138,31 @@ class StaffAccounts
|
||||
&& Role::query()->whereKey($roleId)->where('is_administrator', true)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* The installation's principal administrator: the oldest active one.
|
||||
*
|
||||
* "Oldest" is the account created first, which on any installation
|
||||
* that went through setup is the person who set it up — and on one
|
||||
* imported from v1, the first administrator that import created.
|
||||
* There is no *flag* for this because the application deliberately has
|
||||
* no owner concept: administrators are equal in authority, and
|
||||
* inventing a superior one to hold this would be a real change to the
|
||||
* permission model in exchange for a greeting.
|
||||
*
|
||||
* Active, so that a founder who has since left the company does not
|
||||
* silently swallow anything addressed here; it moves to the next
|
||||
* administrator instead.
|
||||
*/
|
||||
public function mainAdministrator(): ?User
|
||||
{
|
||||
return User::query()
|
||||
->where('type', UserType::Staff)
|
||||
->where('active', true)
|
||||
->whereHas('role', fn ($query) => $query->where('is_administrator', true))
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* How many staff are active administrators right now — used to flag
|
||||
* the sole one in the UI so its delete/demote/deactivate controls
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Notifications\Console;
|
||||
|
||||
use App\Modules\Notifications\InAppNotification;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Delete read notifications past the retention window.
|
||||
*
|
||||
* One row per recipient per event, and until now nothing ever removed
|
||||
* one. On an installation where every share notifies a handful of people
|
||||
* this is the fastest-growing table in the database, and the growth buys
|
||||
* nothing: nobody scrolls a year back through a notification list.
|
||||
*
|
||||
* **Unread notifications are never deleted, at any age.** A notification
|
||||
* nobody has looked at is the one thing in this table still doing its
|
||||
* job, and an installation whose owner was away for four months should
|
||||
* come back to their news rather than to a clean slate. The history is
|
||||
* not an audit trail either way — the activity log is, and it is never
|
||||
* pruned.
|
||||
*/
|
||||
class PurgeNotificationsCommand extends Command
|
||||
{
|
||||
protected $signature = 'projectsend:purge-notifications';
|
||||
|
||||
protected $description = 'Delete read notifications older than the configured retention window (runs daily)';
|
||||
|
||||
public function handle(Settings $settings): int
|
||||
{
|
||||
$days = (int) $settings->get(Setting::NotificationRetentionDays);
|
||||
|
||||
if ($days <= 0) {
|
||||
$this->info('Notification retention is disabled; nothing pruned.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Chunked for the same reason the API request log is: this is a
|
||||
// high-volume table, and a single unbounded DELETE on a busy
|
||||
// installation holds locks for as long as it takes.
|
||||
$cutoff = now()->subDays($days);
|
||||
$deleted = 0;
|
||||
|
||||
do {
|
||||
$batch = InAppNotification::query()
|
||||
->whereNotNull('read_at')
|
||||
->where('created_at', '<', $cutoff)
|
||||
->limit(5000)
|
||||
->delete();
|
||||
$deleted += $batch;
|
||||
} while ($batch > 0);
|
||||
|
||||
$this->info("Pruned {$deleted} read notifications older than {$days} days.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ class NotificationsServiceProvider extends ServiceProvider
|
||||
$this->app->singleton(NotificationTypeRegistry::class);
|
||||
$this->app->singleton(Notifier::class);
|
||||
$this->app->singleton(NotificationPreferences::class);
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
Console\PurgeNotificationsCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
|
||||
@@ -43,6 +43,15 @@ enum Capability: string
|
||||
// package (github.com/projectsend/cloud-modules), never in this repo.
|
||||
case Branding = 'branding.customize';
|
||||
|
||||
// Cloud-only — the storage backend is ours, supplied by the
|
||||
// environment when the instance is provisioned and not the customer's
|
||||
// to see or change. The counterpart of StorageConfigure above rather
|
||||
// than a contradiction of it: one edition configures its own bucket,
|
||||
// the other is given one. Behaviour lives in the private
|
||||
// projectsend/cloud-modules package; without it this capability is
|
||||
// simply inert and files stay on local disk.
|
||||
case StorageManaged = 'storage.managed';
|
||||
|
||||
// Cloud-only — managed installations supply CAPTCHA keys centrally, so
|
||||
// protection is on before anybody finds the settings screen. The
|
||||
// feature itself is in both editions and behind no capability: this
|
||||
@@ -64,6 +73,7 @@ enum Capability: string
|
||||
self::CustomAssets => [Edition::Community],
|
||||
|
||||
self::Branding,
|
||||
self::StorageManaged,
|
||||
self::CaptchaManagedKeys => [Edition::Cloud],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace App\Modules\Platform\Http\Controllers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Platform\Capabilities\Capability;
|
||||
use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use App\Modules\Platform\System\SystemEnvironment;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@@ -32,6 +34,7 @@ class AboutController extends Controller
|
||||
public function __construct(
|
||||
private readonly CapabilityRegistry $capabilities,
|
||||
private readonly SystemEnvironment $environment,
|
||||
private readonly Settings $settings,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
@@ -45,6 +48,35 @@ class AboutController extends Controller
|
||||
return Inertia::render('system/about', [
|
||||
'license' => 'GNU General Public License v2',
|
||||
'environment' => $canSeeEnvironment ? $this->environment->toArray() : null,
|
||||
'updated' => $canSeeEnvironment ? $this->lastUpdate() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* When `projectsend:update` last brought this installation to a new
|
||||
* version.
|
||||
*
|
||||
* Both values are written by that command alone, and RunningCodeState
|
||||
* has until now been their only reader — which means the fact was
|
||||
* recorded and then only ever mentioned when something was *wrong*.
|
||||
* On a healthy installation nothing said when it was last updated,
|
||||
* which is the ordinary question of the two.
|
||||
*
|
||||
* Null on anything that has never been updated through the command: a
|
||||
* fresh install, or one older than the command itself. There is no
|
||||
* honest date to show there, and "unknown" is noise.
|
||||
*
|
||||
* @return array{version: string, at: string}|null
|
||||
*/
|
||||
private function lastUpdate(): ?array
|
||||
{
|
||||
$version = $this->settings->get(Setting::AppliedVersion);
|
||||
$at = $this->settings->get(Setting::AppliedVersionAt);
|
||||
|
||||
if (! is_string($version) || $version === '' || ! is_string($at) || $at === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ['version' => $version, 'at' => $at];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,17 @@ use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Platform\Settings\ExternalStorageConfigApplier;
|
||||
use App\Modules\Platform\Settings\ExternalStorageSettings;
|
||||
use App\Modules\Platform\Settings\StorageProvider;
|
||||
use Aws\S3\S3Client;
|
||||
use Closure;
|
||||
use Google\Cloud\Storage\StorageClient;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -37,6 +42,7 @@ class ExternalStorageSettingsController extends Controller
|
||||
|
||||
return Inertia::render('system/settings/storage', [
|
||||
'active' => $settings->active,
|
||||
'provider' => $settings->provider->value,
|
||||
// Never name a top-level Inertia prop "key" — Inertia's React
|
||||
// renderer spreads page props onto the component via
|
||||
// `{ key: <internal-remount-key>, ...props }`, and a prop
|
||||
@@ -45,6 +51,10 @@ class ExternalStorageSettingsController extends Controller
|
||||
// the component as an actual prop (React always strips `key`).
|
||||
'access_key' => $settings->key ?? '',
|
||||
'has_secret' => $settings->secret !== null && $settings->secret !== '',
|
||||
// Same treatment as the secret: never round-tripped, only
|
||||
// whether one is stored. A service account key file is more
|
||||
// sensitive than an access key, not less.
|
||||
'has_key_file' => $settings->key_file !== null && $settings->key_file !== '',
|
||||
'bucket' => $settings->bucket ?? '',
|
||||
'region' => $settings->region ?? '',
|
||||
'endpoint' => $settings->endpoint ?? '',
|
||||
@@ -56,35 +66,56 @@ class ExternalStorageSettingsController extends Controller
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$request->merge(['provider' => $request->input('provider', StorageProvider::S3->value)]);
|
||||
|
||||
$validated = $request->validate([
|
||||
'active' => ['required', 'boolean'],
|
||||
'access_key' => ['required', 'string', 'max:255'],
|
||||
'secret' => ['nullable', 'string', 'max:255'],
|
||||
// 'sometimes', not 'required': absent means S3, which is what
|
||||
// every payload written before this choice existed meant, and
|
||||
// stops a browser holding a stale bundle from failing to save
|
||||
// on a field it cannot see.
|
||||
'provider' => ['sometimes', Rule::enum(StorageProvider::class)],
|
||||
'bucket' => ['required', 'string', 'max:255'],
|
||||
'region' => ['required', 'string', 'max:255'],
|
||||
'root' => ['nullable', 'string', 'max:255'],
|
||||
|
||||
// Required only for the provider that uses them, so switching
|
||||
// to GCS does not demand an AWS region that means nothing.
|
||||
'access_key' => ['required_if:provider,s3', 'nullable', 'string', 'max:255'],
|
||||
'secret' => ['nullable', 'string', 'max:255'],
|
||||
'region' => ['required_if:provider,s3', 'nullable', 'string', 'max:255'],
|
||||
'endpoint' => ['nullable', 'string', 'max:255'],
|
||||
'use_path_style' => ['required', 'boolean'],
|
||||
'root' => ['nullable', 'string', 'max:255'],
|
||||
|
||||
// Checked for shape here rather than left to fail at the first
|
||||
// upload: a key file is pasted, and a paste that lost its last
|
||||
// line is the likeliest way this goes wrong.
|
||||
'key_file' => ['nullable', 'string', self::serviceAccountKeyRule()],
|
||||
]);
|
||||
|
||||
$settings = ExternalStorageSettings::current();
|
||||
|
||||
$settings->fill([
|
||||
'active' => $validated['active'],
|
||||
'key' => $validated['access_key'],
|
||||
'provider' => $validated['provider'],
|
||||
'key' => $validated['access_key'] ?? null,
|
||||
'bucket' => $validated['bucket'],
|
||||
'region' => $validated['region'],
|
||||
'region' => $validated['region'] ?? null,
|
||||
'endpoint' => $validated['endpoint'] ?? null,
|
||||
'use_path_style' => $validated['use_path_style'],
|
||||
'root' => $validated['root'] ?? null,
|
||||
]);
|
||||
|
||||
// A blank secret keeps whatever is already stored — the field is
|
||||
// never round-tripped to the browser (only `has_secret` is).
|
||||
// A blank credential keeps whatever is already stored — neither
|
||||
// field is ever round-tripped to the browser (only the has_*
|
||||
// flags are), so blank means "unchanged", not "cleared".
|
||||
if (is_string($validated['secret'] ?? null) && $validated['secret'] !== '') {
|
||||
$settings->secret = $validated['secret'];
|
||||
}
|
||||
|
||||
if (is_string($validated['key_file'] ?? null) && $validated['key_file'] !== '') {
|
||||
$settings->key_file = $validated['key_file'];
|
||||
}
|
||||
|
||||
$settings->save();
|
||||
|
||||
$this->configApplier->flush();
|
||||
@@ -101,43 +132,31 @@ class ExternalStorageSettingsController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the submitted (or, if the secret field was left blank, the
|
||||
* already-stored) credentials can actually reach the bucket, mirroring
|
||||
* v1's connection test — this exists specifically to catch a typo'd
|
||||
* key/bucket/region before switching uploads over to it.
|
||||
* Verifies the submitted (or, where a credential field was left
|
||||
* blank, the already-stored) details can actually reach the bucket,
|
||||
* mirroring v1's connection test — this exists specifically to catch
|
||||
* a typo'd key/bucket/region before switching uploads over to it.
|
||||
*/
|
||||
public function testConnection(Request $request): RedirectResponse
|
||||
{
|
||||
$request->merge(['provider' => $request->input('provider', StorageProvider::S3->value)]);
|
||||
|
||||
$validated = $request->validate([
|
||||
'access_key' => ['required', 'string', 'max:255'],
|
||||
'secret' => ['nullable', 'string', 'max:255'],
|
||||
'provider' => ['sometimes', Rule::enum(StorageProvider::class)],
|
||||
'bucket' => ['required', 'string', 'max:255'],
|
||||
'region' => ['required', 'string', 'max:255'],
|
||||
'access_key' => ['required_if:provider,s3', 'nullable', 'string', 'max:255'],
|
||||
'secret' => ['nullable', 'string', 'max:255'],
|
||||
'region' => ['required_if:provider,s3', 'nullable', 'string', 'max:255'],
|
||||
'endpoint' => ['nullable', 'string', 'max:255'],
|
||||
'use_path_style' => ['nullable', 'boolean'],
|
||||
'key_file' => ['nullable', 'string', self::serviceAccountKeyRule()],
|
||||
]);
|
||||
|
||||
$settings = ExternalStorageSettings::current();
|
||||
$secret = (is_string($validated['secret'] ?? null) && $validated['secret'] !== '')
|
||||
? $validated['secret']
|
||||
: $settings->secret;
|
||||
|
||||
try {
|
||||
$config = [
|
||||
'version' => 'latest',
|
||||
'region' => $validated['region'],
|
||||
'credentials' => [
|
||||
'key' => $validated['access_key'],
|
||||
'secret' => (string) $secret,
|
||||
],
|
||||
'use_path_style_endpoint' => (bool) ($validated['use_path_style'] ?? false),
|
||||
];
|
||||
|
||||
if (is_string($validated['endpoint'] ?? null) && $validated['endpoint'] !== '') {
|
||||
$config['endpoint'] = $validated['endpoint'];
|
||||
}
|
||||
|
||||
(new S3Client($config))->headBucket(['Bucket' => $validated['bucket']]);
|
||||
match (StorageProvider::from($validated['provider'])) {
|
||||
StorageProvider::S3 => $this->probeS3($validated),
|
||||
StorageProvider::Gcs => $this->probeGcs($validated),
|
||||
};
|
||||
|
||||
$result = __('Success: connected to bucket ":bucket".', ['bucket' => $validated['bucket']]);
|
||||
} catch (Throwable $e) {
|
||||
@@ -146,4 +165,91 @@ class ExternalStorageSettingsController extends Controller
|
||||
|
||||
return back()->with('storage_test_result', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
private function probeS3(array $validated): void
|
||||
{
|
||||
$config = [
|
||||
'version' => 'latest',
|
||||
'region' => $validated['region'],
|
||||
'credentials' => [
|
||||
'key' => $validated['access_key'],
|
||||
'secret' => (string) $this->storedIfBlank($validated, 'secret'),
|
||||
],
|
||||
'use_path_style_endpoint' => (bool) ($validated['use_path_style'] ?? false),
|
||||
];
|
||||
|
||||
if (is_string($validated['endpoint'] ?? null) && $validated['endpoint'] !== '') {
|
||||
$config['endpoint'] = $validated['endpoint'];
|
||||
}
|
||||
|
||||
(new S3Client($config))->headBucket(['Bucket' => $validated['bucket']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
private function probeGcs(array $validated): void
|
||||
{
|
||||
$keyFile = json_decode((string) $this->storedIfBlank($validated, 'key_file'), true);
|
||||
|
||||
if (! is_array($keyFile)) {
|
||||
throw new RuntimeException(__('No service account key has been saved yet.'));
|
||||
}
|
||||
|
||||
$bucket = (new StorageClient(['keyFile' => $keyFile]))->bucket($validated['bucket']);
|
||||
|
||||
// Listing one object rather than asking whether the bucket exists.
|
||||
// A least-privilege key — roles/storage.objectAdmin scoped to this
|
||||
// bucket, which is what the whole design rests on — can read and
|
||||
// write objects but cannot read the bucket's own metadata, so
|
||||
// $bucket->exists() reports failure for a key that works perfectly.
|
||||
// An empty bucket is a valid answer here, and returns no rows.
|
||||
iterator_to_array($bucket->objects(['maxResults' => 1]), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential field left blank means "keep what is stored" on save,
|
||||
* so the connection test has to read it the same way — otherwise
|
||||
* testing an unchanged configuration would always fail.
|
||||
*
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
private function storedIfBlank(array $validated, string $field): ?string
|
||||
{
|
||||
$submitted = $validated[$field] ?? null;
|
||||
|
||||
if (is_string($submitted) && $submitted !== '') {
|
||||
return $submitted;
|
||||
}
|
||||
|
||||
return ExternalStorageSettings::current()->{$field};
|
||||
}
|
||||
|
||||
/**
|
||||
* A pasted service account key, checked for the parts that have to be
|
||||
* there. Not a credential check — that is what Test connection is for.
|
||||
*/
|
||||
private static function serviceAccountKeyRule(): Closure
|
||||
{
|
||||
return function (string $attribute, mixed $value, Closure $fail): void {
|
||||
$decoded = json_decode((string) $value, true);
|
||||
|
||||
if (! is_array($decoded)) {
|
||||
$fail(__('That does not look like a service account key file: it is not valid JSON.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (['client_email', 'private_key'] as $required) {
|
||||
if (! isset($decoded[$required]) || ! is_string($decoded[$required]) || $decoded[$required] === '') {
|
||||
$fail(__('That service account key file is missing its :field.', ['field' => $required]));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Platform\Onboarding\InstallationWelcome;
|
||||
use App\Modules\Platform\Onboarding\QuickStart;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
/**
|
||||
* The screen a brand-new installation opens on: thank you, then the short
|
||||
* list of things worth doing first, then — at the very bottom, once there
|
||||
* is nothing left to do here — the invitation to the Discord.
|
||||
*
|
||||
* That order is the point. Somebody who has just installed this has a job
|
||||
* in mind, and the fastest way to lose them is to open with a social
|
||||
* invitation instead of the thing they came to do. The invitation is
|
||||
* still worth making; it just belongs after the work, not in front of it.
|
||||
*/
|
||||
class GettingStartedController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InstallationWelcome $welcome,
|
||||
private readonly QuickStart $quickStart,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
assert($user !== null);
|
||||
|
||||
$props = [
|
||||
'items' => $this->quickStart->forUser($user),
|
||||
'justInstalled' => $this->welcome->pending(),
|
||||
];
|
||||
|
||||
// Only for the person it was addressed to — another staff member
|
||||
// reading it later is reading, not receiving.
|
||||
if ($this->welcome->isWaitingFor($user)) {
|
||||
$this->welcome->dismiss();
|
||||
}
|
||||
|
||||
return Inertia::render('system/getting-started', $props);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class PublicListingSettingsController extends Controller
|
||||
return Inertia::render('system/settings/public-listing', [
|
||||
'public_listing_enabled' => $this->settings->get(Setting::PublicListingEnabled),
|
||||
'public_listing_slug' => $this->settings->get(Setting::PublicListingSlug),
|
||||
'public_listing_preview_enabled' => $this->settings->get(Setting::PublicListingPreviewEnabled),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -42,10 +43,12 @@ class PublicListingSettingsController extends Controller
|
||||
$validated = $request->validate([
|
||||
'public_listing_enabled' => ['required', 'boolean'],
|
||||
'public_listing_slug' => ['required', 'string', 'max:255', 'regex:/^[a-z0-9]+(-[a-z0-9]+)*$/'],
|
||||
'public_listing_preview_enabled' => ['required', 'boolean'],
|
||||
]);
|
||||
|
||||
$this->settings->set(Setting::PublicListingEnabled, $validated['public_listing_enabled']);
|
||||
$this->settings->set(Setting::PublicListingSlug, $validated['public_listing_slug']);
|
||||
$this->settings->set(Setting::PublicListingPreviewEnabled, $validated['public_listing_preview_enabled']);
|
||||
|
||||
$this->activity->log(Action::SettingsUpdated, context: ['section' => 'public_listing']);
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace App\Modules\Platform\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Platform\Scheduling\ScheduledTaskRun;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use App\Modules\Platform\Updates\LatestReleaseInfo;
|
||||
use App\Support\Pagination;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -32,29 +35,49 @@ class SchedulerMonitoringController extends Controller
|
||||
* in sync with 7 already-scattered `protected $signature` values
|
||||
* either way.
|
||||
*
|
||||
* @var array<string, string>
|
||||
* A method rather than a const because the labels are prose and a
|
||||
* const cannot call __(). Held as data under a key, they were
|
||||
* invisible to the translation scan — which only sees literals inside
|
||||
* __() — so this screen listed ten English rows in the middle of an
|
||||
* otherwise translated page, and nothing reported it as missing.
|
||||
*
|
||||
* The keys are the command names, and they are what everything else
|
||||
* matches on: the run history, the detail map, the frontend, and the
|
||||
* test asserting this list and the schedule are the same list. Only
|
||||
* the values here are language.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private const KNOWN_COMMANDS = [
|
||||
'projectsend:purge-erasures' => 'Purge erased accounts',
|
||||
'projectsend:purge-stale-uploads' => 'Purge stale chunked uploads',
|
||||
'projectsend:purge-zip-downloads' => 'Purge zip downloads',
|
||||
'projectsend:check-for-updates' => 'Check for updates',
|
||||
'projectsend:fetch-news' => 'Fetch dashboard news',
|
||||
'projectsend:purge-expired-files' => 'Purge expired files',
|
||||
'projectsend:purge-orphan-files' => 'Purge orphan files',
|
||||
];
|
||||
private function knownCommands(): array
|
||||
{
|
||||
return [
|
||||
'projectsend:purge-erasures' => (string) __('Purge erased accounts'),
|
||||
'projectsend:purge-stale-uploads' => (string) __('Purge stale chunked uploads'),
|
||||
'projectsend:purge-zip-downloads' => (string) __('Purge zip downloads'),
|
||||
'projectsend:check-for-updates' => (string) __('Check for updates'),
|
||||
'projectsend:fetch-news' => (string) __('Fetch dashboard news'),
|
||||
'projectsend:purge-expired-files' => (string) __('Purge expired files'),
|
||||
'projectsend:purge-orphan-files' => (string) __('Purge orphan files'),
|
||||
'projectsend:purge-api-request-logs' => (string) __('Purge API request logs'),
|
||||
'projectsend:purge-failed-jobs' => (string) __('Purge failed jobs'),
|
||||
'projectsend:purge-notifications' => (string) __('Purge read notifications'),
|
||||
];
|
||||
}
|
||||
|
||||
private const FAILED_PER_PAGE = 20;
|
||||
|
||||
public function __construct(
|
||||
private readonly FailedJobProviderInterface $failer,
|
||||
private readonly LatestReleaseInfo $latestRelease,
|
||||
private readonly Settings $settings,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response|RedirectResponse
|
||||
{
|
||||
$runs = ScheduledTaskRun::query()->get()->keyBy('command');
|
||||
$details = $this->details();
|
||||
|
||||
$tasks = collect(self::KNOWN_COMMANDS)->map(function (string $label, string $command) use ($runs): array {
|
||||
$tasks = collect($this->knownCommands())->map(function (string $label, string $command) use ($runs, $details): array {
|
||||
$run = $runs->get($command);
|
||||
|
||||
return [
|
||||
@@ -62,6 +85,7 @@ class SchedulerMonitoringController extends Controller
|
||||
'label' => $label,
|
||||
'status' => $run?->status->value,
|
||||
'message' => $run?->message,
|
||||
'detail' => $details[$command] ?? null,
|
||||
'duration_ms' => $run?->duration_ms,
|
||||
'ran_at' => $run?->ran_at?->toIso8601String(),
|
||||
];
|
||||
@@ -110,9 +134,36 @@ class SchedulerMonitoringController extends Controller
|
||||
'failed_pagination' => Pagination::meta($paginator),
|
||||
'failed_total' => $failed->count(),
|
||||
'pending_jobs_count' => DB::table('jobs')->count(),
|
||||
'retention' => [
|
||||
'failed_jobs' => (int) $this->settings->get(Setting::FailedJobRetentionDays),
|
||||
'notifications' => (int) $this->settings->get(Setting::NotificationRetentionDays),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* How long the two tables that grow on their own are kept.
|
||||
*
|
||||
* They live on this screen because this is where their purges are
|
||||
* listed: the window and the job that honours it are one idea, and an
|
||||
* administrator who has just seen "Purge failed jobs · never run" is
|
||||
* the person asking how long anything is kept.
|
||||
*/
|
||||
public function updateRetention(Request $request): RedirectResponse
|
||||
{
|
||||
// Ten years is not a policy, it is a guard against a typo becoming
|
||||
// a number nobody notices. 0 is the real "keep everything".
|
||||
$validated = $request->validate([
|
||||
'failed_jobs' => ['required', 'integer', 'min:0', 'max:3650'],
|
||||
'notifications' => ['required', 'integer', 'min:0', 'max:3650'],
|
||||
]);
|
||||
|
||||
$this->settings->set(Setting::FailedJobRetentionDays, $validated['failed_jobs']);
|
||||
$this->settings->set(Setting::NotificationRetentionDays, $validated['notifications']);
|
||||
|
||||
return back()->with('success', __('Retention updated.'));
|
||||
}
|
||||
|
||||
public function retryFailedJob(string $uuid): RedirectResponse
|
||||
{
|
||||
Artisan::call('queue:retry', ['id' => [$uuid]]);
|
||||
@@ -137,4 +188,32 @@ class SchedulerMonitoringController extends Controller
|
||||
|
||||
return back()->with('success', __('All failed jobs deleted.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* What a task actually found, for the tasks that find something.
|
||||
*
|
||||
* The run rows answer "did it work"; they cannot answer "and what did
|
||||
* it say", because Laravel's ScheduledTaskFinished event fires after
|
||||
* the command returns and carries no output — which is why a
|
||||
* successful check for updates has always shown an empty Message.
|
||||
* Joined here at render time instead, from what the command itself
|
||||
* wrote to the settings, so the line stays true whether the daily job
|
||||
* or somebody pressing the button did the work.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function details(): array
|
||||
{
|
||||
$release = $this->latestRelease->current();
|
||||
$checkedAt = $this->settings->get(Setting::LatestVersionCheckedAt);
|
||||
$everChecked = is_string($checkedAt) && $checkedAt !== '';
|
||||
|
||||
$updates = match (true) {
|
||||
$release !== null => (string) __(':version is available', ['version' => $release['version']]),
|
||||
$everChecked => (string) __('Up to date'),
|
||||
default => null,
|
||||
};
|
||||
|
||||
return array_filter(['projectsend:check-for-updates' => $updates]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
||||
use App\Modules\Platform\Localization\TimezoneRegistry;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use App\Modules\Platform\Updates\CheckForUpdates;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -25,6 +27,13 @@ use Inertia\Response;
|
||||
*/
|
||||
class SystemSettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* How long an answer from the release feed is treated as still true.
|
||||
* Short enough that "check now" means now, long enough that a room
|
||||
* full of administrators cannot spend the server's whole allowance.
|
||||
*/
|
||||
private const CHECK_COOLDOWN_MINUTES = 5;
|
||||
|
||||
public function __construct(
|
||||
private readonly Settings $settings,
|
||||
private readonly ActivityLogger $activity,
|
||||
@@ -53,9 +62,63 @@ class SystemSettingsController extends Controller
|
||||
'viewer_timezone' => $request->user()?->timezone,
|
||||
'can_manage_updates' => $canManageUpdates,
|
||||
'check_for_updates' => $canManageUpdates ? $this->settings->get(Setting::CheckForUpdates) : null,
|
||||
'last_checked_at' => $canManageUpdates ? $this->lastCheckedAt()?->toIso8601String() : null,
|
||||
'check_result' => $request->session()->get('update_check_result'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the release feed now, rather than waiting for tonight's run.
|
||||
*
|
||||
* Deliberately not gated on Setting::CheckForUpdates: that switches
|
||||
* off the unattended daily call, which is not the same as refusing to
|
||||
* answer a question somebody has just asked out loud.
|
||||
*/
|
||||
public function checkForUpdates(Request $request, CheckForUpdates $check): RedirectResponse
|
||||
{
|
||||
$canManageUpdates = $this->capabilities->has(Capability::SystemUpdates)
|
||||
&& $request->user()?->can('manage_updates') === true;
|
||||
|
||||
abort_unless($canManageUpdates, 403);
|
||||
|
||||
// A second throttle, and not a redundant one. The route's bucket is
|
||||
// per user; GitHub's unauthenticated rate limit is per server
|
||||
// address, so two administrators each within their own allowance
|
||||
// can still exhaust the installation's. This one is the whole
|
||||
// installation's, and it costs no new setting — the timestamp it
|
||||
// reads is the one every check already writes.
|
||||
$lastCheckedAt = $this->lastCheckedAt();
|
||||
|
||||
if ($lastCheckedAt !== null && $lastCheckedAt->gt(now()->subMinutes(self::CHECK_COOLDOWN_MINUTES))) {
|
||||
return back()->with('update_check_result', [
|
||||
'ok' => true,
|
||||
'message' => __('Checked a moment ago, so this is the answer from then.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $check->run();
|
||||
|
||||
return back()->with('update_check_result', [
|
||||
'ok' => $result['ok'],
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function lastCheckedAt(): ?CarbonImmutable
|
||||
{
|
||||
$value = $this->settings->get(Setting::LatestVersionCheckedAt);
|
||||
|
||||
if (! is_string($value) || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return CarbonImmutable::parse($value);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$canManageUpdates = $this->capabilities->has(Capability::SystemUpdates)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Platform\Updates\UpdateWelcome;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
/**
|
||||
* The screen an administrator lands on the first time they open
|
||||
* ProjectSend after an update: what it is now running, an invitation to
|
||||
* the place where the people are, and then what the release actually
|
||||
* brought.
|
||||
*
|
||||
* That order is the design. The thanks and the invitation are for the
|
||||
* person who just did the work — a moment that exists exactly once per
|
||||
* update and is otherwise spent staring at a dashboard that looks
|
||||
* identical to yesterday's. The release notes are underneath because
|
||||
* they are the part that can be read later, or not at all.
|
||||
*/
|
||||
class WhatsNewController extends Controller
|
||||
{
|
||||
public function __construct(private readonly UpdateWelcome $welcome) {}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
assert($user !== null);
|
||||
|
||||
// Read before dismissing: everything below describes the update
|
||||
// that the next line forgets.
|
||||
$props = [
|
||||
'version' => $this->welcome->version(),
|
||||
'previousVersion' => $this->welcome->previousVersion(),
|
||||
'justUpdated' => $this->welcome->isFresh(),
|
||||
'releases' => $this->welcome->releases(),
|
||||
];
|
||||
|
||||
// Only for the person it was addressed to. Another administrator
|
||||
// opening the page from a link is reading, not receiving, and must
|
||||
// not consume a greeting meant for somebody else.
|
||||
if ($this->welcome->isWaitingFor($user)) {
|
||||
$this->welcome->dismiss();
|
||||
}
|
||||
|
||||
return Inertia::render('system/whats-new', $props);
|
||||
}
|
||||
}
|
||||
@@ -5,35 +5,56 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Platform\Installation;
|
||||
|
||||
/**
|
||||
* Whether this installation runs from a container image or from files on a
|
||||
* server somebody administers directly.
|
||||
* Whether this installation runs from a container image, from a container
|
||||
* the operator builds themselves, or from files on a server somebody
|
||||
* administers directly.
|
||||
*
|
||||
* It exists because the application tells administrators how to upgrade, and
|
||||
* the two answers have nothing in common. A container is replaced —
|
||||
* `docker compose pull && docker compose up -d`, with the entrypoint running
|
||||
* the migrations on the way up. A manual install is a sequence somebody
|
||||
* performs by hand: back up, take the site down, unpack the release over the
|
||||
* directory, migrate, refresh the caches, bring it back (INSTALL.md).
|
||||
* the three answers have nothing in common. A published container is
|
||||
* replaced — `docker compose pull && docker compose up -d`, with the
|
||||
* entrypoint running the migrations on the way up. A container built from a
|
||||
* checkout has to be given new code and rebuilt. A manual install is a
|
||||
* sequence somebody performs by hand: back up, take the site down, unpack
|
||||
* the release over the directory, migrate, refresh the caches, bring it back
|
||||
* (INSTALL.md).
|
||||
*
|
||||
* Printing the container command to someone who installed from a zip is
|
||||
* worse than printing nothing: it names a tool they do not have, for a stack
|
||||
* they are not running, at the exact moment they are trying to do the right
|
||||
* thing. That was the behaviour before this class existed — the command was
|
||||
* a hardcoded string in two React components, written when Docker was the
|
||||
* only supported path.
|
||||
* Printing the wrong one of those is worse than printing nothing. For a
|
||||
* manual install the container command names a tool they do not have, for a
|
||||
* stack they are not running, at the exact moment they are trying to do the
|
||||
* right thing — that was the behaviour before this class existed, when the
|
||||
* command was a hardcoded string in two React components. For a stack built
|
||||
* from a checkout it is worse still, because the command runs: `pull` skips
|
||||
* services that have no image to pull and `up -d` then finds every container
|
||||
* already current, so the update reports success and changes nothing, and
|
||||
* the dashboard goes on offering the same release forever (#1661).
|
||||
*
|
||||
* The detection is the presence of the file a container runtime leaves in
|
||||
* the root filesystem. It is a deliberately conservative signal: something
|
||||
* exotic enough to run neither Docker nor Podman is reported as a manual
|
||||
* install, which is the safer wrong answer of the two — the manual
|
||||
* instructions are steps a person follows and check for themselves, while
|
||||
* the container command is one they would paste.
|
||||
* Two signals, in order:
|
||||
*
|
||||
* 1. The published image sets PROJECTSEND_IMAGE. A positive marker set at
|
||||
* build time is the only one a bind mount can neither forge nor hide.
|
||||
* 2. Failing that — images published before that variable existed — a
|
||||
* working tree in the install directory. The image is built from an
|
||||
* unpacked release artifact and has none; the Compose stack in the
|
||||
* repository bind-mounts the repository itself.
|
||||
*
|
||||
* Being in a container at all is the presence of the file a container
|
||||
* runtime leaves in the root filesystem. It is a deliberately conservative
|
||||
* signal: something exotic enough to run neither Docker nor Podman is
|
||||
* reported as a manual install, which is the safer wrong answer of the
|
||||
* three — the manual instructions are steps a person follows and checks for
|
||||
* themselves, while the container commands are ones they would paste.
|
||||
*/
|
||||
class Installation
|
||||
{
|
||||
public function kind(): InstallationKind
|
||||
{
|
||||
return $this->inContainer() ? InstallationKind::Container : InstallationKind::Manual;
|
||||
if (! $this->inContainer()) {
|
||||
return InstallationKind::Manual;
|
||||
}
|
||||
|
||||
return $this->builtFromSource()
|
||||
? InstallationKind::ContainerSource
|
||||
: InstallationKind::Container;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,6 +64,33 @@ class Installation
|
||||
protected function inContainer(): bool
|
||||
{
|
||||
// Docker writes the first; Podman writes the second.
|
||||
return file_exists('/.dockerenv') || file_exists('/run/.containerenv');
|
||||
//
|
||||
// Suppressed, and it has to stay that way. Shared hosting sets
|
||||
// open_basedir to the webspace, and probing a path outside it is a
|
||||
// warning rather than a false — which the framework's error handler
|
||||
// turns into an exception, so the one call that asks which install
|
||||
// this is took the whole dashboard down with it (#1663). Under `@`
|
||||
// the warning is filtered and the probe answers false, which is the
|
||||
// right answer anyway: a host that restricts PHP to a vhost
|
||||
// directory is not the container image.
|
||||
return @file_exists('/.dockerenv') || @file_exists('/run/.containerenv');
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected for the same reason as inContainer(), and answered the same
|
||||
* way in tests.
|
||||
*/
|
||||
protected function builtFromSource(): bool
|
||||
{
|
||||
// getenv() rather than env(): once the configuration is cached,
|
||||
// env() outside a config file returns null, and the answer would
|
||||
// silently flip on the installs most likely to have cached it.
|
||||
if (getenv('PROJECTSEND_IMAGE') === '1') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A worktree checkout writes .git as a file rather than a
|
||||
// directory, so ask whether it exists, not what it is.
|
||||
return file_exists(base_path('.git'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,17 @@ namespace App\Modules\Platform\Installation;
|
||||
*/
|
||||
enum InstallationKind: string
|
||||
{
|
||||
/** Runs from an image: upgrading is pulling a new one. */
|
||||
/** Runs from the published image: upgrading is pulling a new one. */
|
||||
case Container = 'container';
|
||||
|
||||
/**
|
||||
* Runs from a container the operator builds themselves, out of a
|
||||
* checkout of the repository: upgrading is new code first, then a
|
||||
* rebuild. Pulling does nothing here — there is no published image
|
||||
* behind these containers to pull.
|
||||
*/
|
||||
case ContainerSource = 'container-source';
|
||||
|
||||
/** Runs from files on a server someone administers: upgrading is INSTALL.md's sequence. */
|
||||
case Manual = 'manual';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform;
|
||||
|
||||
use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
||||
use App\Modules\Platform\Capabilities\Edition;
|
||||
|
||||
/**
|
||||
* Where "ProjectSend" points, which is not the same address for both
|
||||
* editions.
|
||||
*
|
||||
* Each edition has its own front door — projectsend.org for the software
|
||||
* you run yourself, projectsend.cloud for the hosted service — and the
|
||||
* link belongs to whichever one the reader is actually using. It matters
|
||||
* beyond the About screen: the "Powered by ProjectSend" line at the foot
|
||||
* of every outgoing email and on every client-facing page is the one
|
||||
* place a recipient meets the product for the first time, and sending a
|
||||
* hosted customer's recipients to the self-hosting instructions is the
|
||||
* wrong door.
|
||||
*
|
||||
* The donation link is dropped on a managed installation for a simpler
|
||||
* reason: those customers are already paying for this. Asking again, on
|
||||
* the screen that thanks them for choosing it, reads as not having
|
||||
* noticed.
|
||||
*
|
||||
* Resolved here rather than in the config file so both readers get the
|
||||
* same answer — the Inertia shared props and the two mail footers, which
|
||||
* read `config('projectsend.links.website')` straight out of Blade.
|
||||
*/
|
||||
class OfficialLinks
|
||||
{
|
||||
public function __construct(private readonly CapabilityRegistry $capabilities) {}
|
||||
|
||||
/**
|
||||
* @return array{website: string, source: string, discord: string, open_collective?: string}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
/** @var array<string, string> $links */
|
||||
$links = config('projectsend.links');
|
||||
|
||||
$resolved = [
|
||||
'website' => $this->website(),
|
||||
'source' => $links['source'] ?? '',
|
||||
'discord' => $links['discord'] ?? '',
|
||||
];
|
||||
|
||||
// Omitted rather than hidden by the page, so a surface added later
|
||||
// cannot ask a paying customer for a donation by forgetting to
|
||||
// check.
|
||||
if (! $this->managed()) {
|
||||
$resolved['open_collective'] = $links['open_collective'] ?? '';
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
public function website(): string
|
||||
{
|
||||
/** @var array<string, string> $links */
|
||||
$links = config('projectsend.links');
|
||||
|
||||
return $this->managed()
|
||||
? ($links['website_cloud'] ?? $links['website'])
|
||||
: $links['website'];
|
||||
}
|
||||
|
||||
private function managed(): bool
|
||||
{
|
||||
return $this->capabilities->edition() === Edition::Cloud;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Onboarding;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Identity\StaffAccounts;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
|
||||
/**
|
||||
* "This installation is new — has its administrator been shown around
|
||||
* yet?" The install-time twin of UpdateWelcome, and deliberately shaped
|
||||
* like it: one marker, one person, one time, and an address that keeps
|
||||
* working afterwards.
|
||||
*
|
||||
* The marker is raised explicitly at the two places a first administrator
|
||||
* comes into existence — the setup screen and `projectsend:admin` — rather
|
||||
* than inferred from an empty database. Inferring it would mean deciding,
|
||||
* on every request forever, whether an installation is "new"; a flag
|
||||
* written once at the only moment the answer is unambiguous costs one
|
||||
* boolean and cannot drift. A third provisioning path added later has to
|
||||
* call raise() too, which is why it lives here rather than being copied
|
||||
* into both callers.
|
||||
*/
|
||||
class InstallationWelcome
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Settings $settings,
|
||||
private readonly StaffAccounts $staff,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Record that this installation was just installed.
|
||||
*/
|
||||
public function raise(): void
|
||||
{
|
||||
$this->settings->set(Setting::GettingStartedPending, true);
|
||||
}
|
||||
|
||||
public function pending(): bool
|
||||
{
|
||||
return $this->settings->get(Setting::GettingStartedPending) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this user should be taken to the page right now.
|
||||
*/
|
||||
public function isWaitingFor(User $user): bool
|
||||
{
|
||||
return $this->pending()
|
||||
&& $this->mayRead($user)
|
||||
&& $this->staff->mainAdministrator()?->is($user) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this user may open the page at all.
|
||||
*
|
||||
* Any staff member, and no capability gate: the page is a list of
|
||||
* links to screens they can already reach, each one filtered to what
|
||||
* that person may actually do (see QuickStart). Both editions get it —
|
||||
* a managed installation still has a first client to add and a first
|
||||
* file to upload, which is the whole content.
|
||||
*/
|
||||
public function mayRead(User $user): bool
|
||||
{
|
||||
return $user->isStaff();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop redirecting. The page itself keeps working — somebody who
|
||||
* closed it on their way past should be able to find it again.
|
||||
*/
|
||||
public function dismiss(): void
|
||||
{
|
||||
$this->settings->set(Setting::GettingStartedPending, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Onboarding;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Modules\Identity\Permissions\Permission;
|
||||
use App\Modules\Identity\Permissions\PermissionChecker;
|
||||
use App\Modules\Identity\UserType;
|
||||
use App\Modules\Platform\Capabilities\Capability;
|
||||
use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
||||
use App\Modules\Platform\Scheduling\ScheduledTaskRun;
|
||||
|
||||
/**
|
||||
* The short list of things worth doing on a brand-new installation, in
|
||||
* the order they make sense, filtered to what this person can actually
|
||||
* do here.
|
||||
*
|
||||
* Two filters, and both matter. **Permission** keeps the list honest for
|
||||
* anybody who is not an administrator — a link to a screen that answers
|
||||
* 403 is worse than no link. **Capability** keeps it honest per edition:
|
||||
* on a managed installation there are no staff accounts to create, no
|
||||
* mail server to point at and no scheduler to check, because somebody
|
||||
* else does all three. A getting-started list that opens with three tasks
|
||||
* you are not allowed to perform teaches the reader to ignore it.
|
||||
*
|
||||
* Each step also says whether it is **essential**, because they are not
|
||||
* equally urgent and a list that pretends otherwise is a list nobody
|
||||
* reads twice. Essential means the installation does not really work
|
||||
* without it — including the one whose absence is silent: nothing tells
|
||||
* you that password resets are going nowhere until somebody needs one.
|
||||
* It is a fact about the task rather than about the layout, which is why
|
||||
* it is decided here and not in the page.
|
||||
*
|
||||
* The tasks that can be answered from the database are answered: "add
|
||||
* your first client", "upload a file" and "check the scheduler" tick
|
||||
* themselves. Nothing else is checkable without guessing — a theme that
|
||||
* was never changed is indistinguishable from one that was chosen
|
||||
* deliberately — and a tick that means "we assume so" is worse than no
|
||||
* tick at all.
|
||||
*
|
||||
* Descriptions are one short line each. This is a list to be scanned on
|
||||
* the first day, by somebody who wants to get on with it; the screen at
|
||||
* the other end of the link explains itself.
|
||||
*/
|
||||
class QuickStart
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CapabilityRegistry $capabilities,
|
||||
private readonly PermissionChecker $permissions,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return list<array{key: string, title: string, description: string, href: string, essential: bool, done: bool}>
|
||||
*/
|
||||
public function forUser(User $user): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
if ($this->permissions->allows($user, Permission::CreateClients)) {
|
||||
$items[] = [
|
||||
'key' => 'client',
|
||||
'title' => __('Add your first client'),
|
||||
'description' => __('The people you send files to.'),
|
||||
'href' => route('clients.create', absolute: false),
|
||||
'essential' => true,
|
||||
'done' => $this->hasAClient(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->permissions->allows($user, Permission::Upload)) {
|
||||
$items[] = [
|
||||
'key' => 'upload',
|
||||
'title' => __('Upload a file'),
|
||||
'description' => __('Drop one in and choose who gets it.'),
|
||||
'href' => route('files.create', absolute: false),
|
||||
'essential' => true,
|
||||
'done' => $this->hasAFile(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->permissions->allows($user, Permission::CreateGroups)) {
|
||||
$items[] = [
|
||||
'key' => 'group',
|
||||
'title' => __('Group your clients'),
|
||||
'description' => __('Share once instead of six times.'),
|
||||
'href' => route('groups.create', absolute: false),
|
||||
'essential' => false,
|
||||
'done' => false,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->permissions->allows($user, Permission::EditSettings)) {
|
||||
$items[] = [
|
||||
'key' => 'theme',
|
||||
'title' => __('Choose how your file lists look'),
|
||||
'description' => __('Four layouts for the client-facing pages.'),
|
||||
'href' => route('system-settings.theming.edit', absolute: false),
|
||||
'essential' => false,
|
||||
'done' => false,
|
||||
];
|
||||
|
||||
$items[] = [
|
||||
'key' => 'email-theme',
|
||||
'title' => __('Choose how your email looks'),
|
||||
'description' => __('Previewed on a real message.'),
|
||||
'href' => route('system-settings.theming.edit', ['tab' => 'email'], absolute: false),
|
||||
'essential' => false,
|
||||
'done' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// Community only: on a managed installation the mail server is
|
||||
// ours, and there is nothing here to point anywhere.
|
||||
if ($this->permissions->allows($user, Permission::EditSettings)
|
||||
&& $this->capabilities->has(Capability::EmailTransportConfigure)) {
|
||||
$items[] = [
|
||||
'key' => 'email',
|
||||
'title' => __('Set up your mail server'),
|
||||
'description' => __('Resets and share links go out through it.'),
|
||||
'href' => route('system-settings.email.edit', absolute: false),
|
||||
'essential' => true,
|
||||
'done' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// Community only, and the example the brief named: a managed
|
||||
// installation has no staff accounts of its own to hand out.
|
||||
if ($this->permissions->allows($user, Permission::CreateUsers)
|
||||
&& $this->capabilities->has(Capability::UsersManage)) {
|
||||
$items[] = [
|
||||
'key' => 'team',
|
||||
'title' => __('Add the rest of your team'),
|
||||
'description' => __('Staff accounts, each scoped by its role.'),
|
||||
'href' => route('users.create', absolute: false),
|
||||
'essential' => false,
|
||||
'done' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// Community only: scheduled work is somebody else's problem on a
|
||||
// managed installation, and its screen does not exist there.
|
||||
if ($this->permissions->allows($user, Permission::ViewSystemInfo)
|
||||
&& $this->capabilities->has(Capability::SchedulerMonitoring)) {
|
||||
$items[] = [
|
||||
'key' => 'scheduler',
|
||||
'title' => __('Check the scheduler is running'),
|
||||
'description' => __('Expiring files and queued email need it.'),
|
||||
'href' => route('system-settings.scheduler.index', absolute: false),
|
||||
'essential' => true,
|
||||
'done' => $this->schedulerHasRun(),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function hasAClient(): bool
|
||||
{
|
||||
return User::query()->where('type', UserType::Client)->exists();
|
||||
}
|
||||
|
||||
private function hasAFile(): bool
|
||||
{
|
||||
return File::query()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* The scheduler step is the one essential task nobody could ever
|
||||
* complete: it was hardcoded unticked even though the screen it links
|
||||
* to already answers the question from these rows.
|
||||
*
|
||||
* A row exists once the scheduler has run here at all — which is
|
||||
* precisely what the step asks. Its status does not come into it: a
|
||||
* task that ran and failed still proves cron reaches this
|
||||
* installation, and the Scheduler screen is where that failure is
|
||||
* read.
|
||||
*/
|
||||
private function schedulerHasRun(): bool
|
||||
{
|
||||
return ScheduledTaskRun::query()->exists();
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,17 @@ use App\Modules\Platform\Localization\LocaleRegistry;
|
||||
use App\Modules\Platform\Localization\TimezoneRegistry;
|
||||
use App\Modules\Platform\News\Console\FetchNewsCommand;
|
||||
use App\Modules\Platform\Notifications\ThemedMailChannel;
|
||||
use App\Modules\Platform\Scheduling\Console\PurgeFailedJobsCommand;
|
||||
use App\Modules\Platform\Scheduling\RecordsScheduledTaskRuns;
|
||||
use App\Modules\Platform\Settings\ExternalStorageConfigApplier;
|
||||
use App\Modules\Platform\Settings\MailConfigApplier;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use App\Modules\Platform\Storage\GoogleCloudStorageDriver;
|
||||
use App\Modules\Platform\Theming\Console\GenerateThemePreviewDataCommand;
|
||||
use App\Modules\Platform\Theming\EmailThemeRegistry;
|
||||
use App\Modules\Platform\Theming\PublicThemeRegistry;
|
||||
use App\Modules\Platform\Updates\Console\CheckForUpdatesCommand;
|
||||
use App\Modules\Platform\Updates\Console\UpdateCommand;
|
||||
use Illuminate\Console\Events\ScheduledTaskFailed;
|
||||
use Illuminate\Console\Events\ScheduledTaskFinished;
|
||||
use Illuminate\Notifications\Channels\MailChannel;
|
||||
@@ -71,9 +74,11 @@ class PlatformServiceProvider extends ServiceProvider
|
||||
$this->commands([
|
||||
GenerateThemePreviewDataCommand::class,
|
||||
CheckForUpdatesCommand::class,
|
||||
UpdateCommand::class,
|
||||
FetchNewsCommand::class,
|
||||
DisableCaptchaCommand::class,
|
||||
TestCaptchaCommand::class,
|
||||
PurgeFailedJobsCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -85,6 +90,13 @@ class PlatformServiceProvider extends ServiceProvider
|
||||
// any — a no-op until the Email settings page is actually saved.
|
||||
$this->app->make(MailConfigApplier::class)->apply();
|
||||
|
||||
// Laravel ships no 'gcs' driver, so the disk config the applier
|
||||
// is about to write would resolve to nothing without this. Cheap
|
||||
// and inert on an install that never selects it: extend() only
|
||||
// records a factory, and nothing calls it until something asks
|
||||
// for a disk whose driver is 'gcs'.
|
||||
$this->app->make(GoogleCloudStorageDriver::class)->register();
|
||||
|
||||
// Same idea for the admin-configured external storage backend —
|
||||
// a no-op until the Storage settings page is actually saved. The
|
||||
// listener is what actually redirects new uploads away from the
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Scheduling\Console;
|
||||
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Queue\Failed\FailedJobProviderInterface;
|
||||
use Illuminate\Queue\Failed\PrunableFailedJobProvider;
|
||||
|
||||
/**
|
||||
* Delete permanently failed queue jobs past the retention window.
|
||||
*
|
||||
* The Scheduler screen lists them and offers a "Delete all failed"
|
||||
* button, which is the right tool for a backlog somebody is looking at —
|
||||
* but it is the *only* thing that ever shrank this table. A mail server
|
||||
* down overnight can leave thousands of rows, each carrying a serialized
|
||||
* payload and an exception trace, on an installation whose administrator
|
||||
* has no reason to open that screen for months.
|
||||
*
|
||||
* Deliberately its own `projectsend:` command rather than a scheduled
|
||||
* `queue:prune-failed`, for two reasons: RecordsScheduledTaskRuns only
|
||||
* tracks our own signatures, so anything else runs invisibly; and the
|
||||
* retention window belongs in the settings with the rest of them rather
|
||||
* than in a number written into routes/console.php.
|
||||
*/
|
||||
class PurgeFailedJobsCommand extends Command
|
||||
{
|
||||
protected $signature = 'projectsend:purge-failed-jobs';
|
||||
|
||||
protected $description = 'Delete failed queue jobs older than the configured retention window (runs daily)';
|
||||
|
||||
public function handle(Settings $settings, FailedJobProviderInterface $failer): int
|
||||
{
|
||||
$days = (int) $settings->get(Setting::FailedJobRetentionDays);
|
||||
|
||||
// 0 means keep indefinitely — an explicit choice for somebody who
|
||||
// treats a failed job as evidence rather than as debris.
|
||||
if ($days <= 0) {
|
||||
$this->info('Failed job retention is disabled; nothing pruned.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Both providers this application can be configured with implement
|
||||
// it; the guard is here because the interface it is declared
|
||||
// against does not require it, and a queue driver that cannot
|
||||
// prune should say so rather than fail obscurely.
|
||||
if (! $failer instanceof PrunableFailedJobProvider) {
|
||||
$this->warn('This queue failure store cannot be pruned; nothing done.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$deleted = $failer->prune(now()->subDays($days));
|
||||
|
||||
$this->info("Pruned {$deleted} failed jobs older than {$days} days.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class ExternalStorageConfigApplier
|
||||
// Bumped on any shape change to the resolved array below — a stale
|
||||
// rememberForever value under an old key would otherwise crash every
|
||||
// boot with "Undefined array key" (apply() calls resolve() unconditionally).
|
||||
private const CACHE_KEY = 'platform.external_storage_settings.v1';
|
||||
private const CACHE_KEY = 'platform.external_storage_settings.v2';
|
||||
|
||||
public function __construct(
|
||||
private readonly CapabilityRegistry $capabilities,
|
||||
@@ -57,17 +57,60 @@ class ExternalStorageConfigApplier
|
||||
}
|
||||
|
||||
$resolved = $this->resolve();
|
||||
$provider = StorageProvider::from($resolved['provider']);
|
||||
|
||||
// The driver is part of what gets overwritten, not a constant:
|
||||
// config/filesystems.php ships the disk as an inert 's3' stub, and
|
||||
// this is the only thing that ever makes it anything else.
|
||||
Config::set('filesystems.disks.files_external.driver', $provider->driver());
|
||||
Config::set('filesystems.disks.files_external.bucket', $resolved['bucket']);
|
||||
|
||||
match ($provider) {
|
||||
StorageProvider::S3 => $this->applyS3($resolved),
|
||||
StorageProvider::Gcs => $this->applyGcs($resolved),
|
||||
};
|
||||
|
||||
if ($resolved['root'] !== null) {
|
||||
// Two names for one idea, because the two adapters disagree:
|
||||
// Laravel's S3 driver reads 'root', Flysystem's GCS adapter is
|
||||
// constructed with a 'prefix'. Setting both keeps the settings
|
||||
// screen able to speak of one "folder inside the bucket".
|
||||
Config::set('filesystems.disks.files_external.root', $resolved['root']);
|
||||
Config::set('filesystems.disks.files_external.prefix', $resolved['root']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $resolved
|
||||
*/
|
||||
private function applyS3(array $resolved): void
|
||||
{
|
||||
Config::set('filesystems.disks.files_external.key', $resolved['key']);
|
||||
Config::set('filesystems.disks.files_external.secret', $resolved['secret']);
|
||||
Config::set('filesystems.disks.files_external.region', $resolved['region']);
|
||||
Config::set('filesystems.disks.files_external.bucket', $resolved['bucket']);
|
||||
Config::set('filesystems.disks.files_external.endpoint', $resolved['endpoint']);
|
||||
Config::set('filesystems.disks.files_external.use_path_style_endpoint', $resolved['use_path_style']);
|
||||
}
|
||||
|
||||
if ($resolved['root'] !== null) {
|
||||
Config::set('filesystems.disks.files_external.root', $resolved['root']);
|
||||
}
|
||||
/**
|
||||
* @param array<string, mixed> $resolved
|
||||
*/
|
||||
private function applyGcs(array $resolved): void
|
||||
{
|
||||
// Decoded here rather than stored decoded: the column holds the
|
||||
// key file verbatim, exactly as Google issued it, so that what an
|
||||
// administrator pasted is what can be handed back to them and
|
||||
// compared against the console.
|
||||
$keyFile = json_decode((string) $resolved['key_file'], true);
|
||||
|
||||
Config::set('filesystems.disks.files_external.key_file', is_array($keyFile) ? $keyFile : null);
|
||||
|
||||
// Left over from the S3 stub in config/filesystems.php, and
|
||||
// meaningless to the GCS adapter — cleared rather than left
|
||||
// sitting there looking like configuration.
|
||||
Config::set('filesystems.disks.files_external.key', null);
|
||||
Config::set('filesystems.disks.files_external.secret', null);
|
||||
Config::set('filesystems.disks.files_external.endpoint', null);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
@@ -99,13 +142,15 @@ class ExternalStorageConfigApplier
|
||||
* filled in and active, nothing more. Callers AND the capability check
|
||||
* live and uncached — see class docblock.
|
||||
*
|
||||
* @return array{configured: bool, key: string|null, secret: string|null, region: string|null, bucket: string|null, endpoint: string|null, use_path_style: bool, root: string|null}
|
||||
* @return array{configured: bool, provider: string, key: string|null, secret: string|null, key_file: string|null, region: string|null, bucket: string|null, endpoint: string|null, use_path_style: bool, root: string|null}
|
||||
*/
|
||||
private function resolve(): array
|
||||
{
|
||||
$blank = [
|
||||
'configured' => false,
|
||||
'key' => null, 'secret' => null, 'region' => null, 'bucket' => null,
|
||||
'provider' => StorageProvider::S3->value,
|
||||
'key' => null, 'secret' => null, 'key_file' => null,
|
||||
'region' => null, 'bucket' => null,
|
||||
'endpoint' => null, 'use_path_style' => false, 'root' => null,
|
||||
];
|
||||
|
||||
@@ -126,8 +171,10 @@ class ExternalStorageConfigApplier
|
||||
|
||||
return [
|
||||
'configured' => true,
|
||||
'provider' => $settings->provider->value,
|
||||
'key' => $settings->key,
|
||||
'secret' => $settings->secret,
|
||||
'key_file' => $settings->key_file,
|
||||
'region' => $settings->region,
|
||||
'bucket' => $settings->bucket,
|
||||
'endpoint' => $settings->endpoint,
|
||||
|
||||
@@ -7,16 +7,19 @@ namespace App\Modules\Platform\Settings;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Admin-configured S3-compatible external storage backend, editable from
|
||||
* the Storage settings page. Single row (id 1 in practice, never
|
||||
* Admin-configured external storage backend — S3-compatible or Google
|
||||
* Cloud Storage, see StorageProvider — editable from the Storage
|
||||
* settings page. Single row (id 1 in practice, never
|
||||
* enforced) — same reasoning as MailProviderSettings: `secret` needs real
|
||||
* Eloquent encryption, which the generic settings table can't offer
|
||||
* per-key.
|
||||
*
|
||||
* @property int $id
|
||||
* @property bool $active
|
||||
* @property StorageProvider $provider
|
||||
* @property string|null $key
|
||||
* @property string|null $secret
|
||||
* @property string|null $key_file
|
||||
* @property string|null $bucket
|
||||
* @property string|null $region
|
||||
* @property string|null $endpoint
|
||||
@@ -29,8 +32,10 @@ class ExternalStorageSettings extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'active',
|
||||
'provider',
|
||||
'key',
|
||||
'secret',
|
||||
'key_file',
|
||||
'bucket',
|
||||
'region',
|
||||
'endpoint',
|
||||
@@ -38,11 +43,28 @@ class ExternalStorageSettings extends Model
|
||||
'root',
|
||||
];
|
||||
|
||||
/**
|
||||
* current() builds this with firstOrNew(), which does not apply the
|
||||
* column defaults — so on an install that has never opened the
|
||||
* Storage screen, `provider` would be null and the match in
|
||||
* isConfigured() would throw rather than answer. Defaults here are
|
||||
* what make an unsaved row a coherent object.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $attributes = [
|
||||
'active' => false,
|
||||
'provider' => 's3',
|
||||
'use_path_style' => false,
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'active' => 'boolean',
|
||||
'provider' => StorageProvider::class,
|
||||
'secret' => 'encrypted',
|
||||
'key_file' => 'encrypted',
|
||||
'use_path_style' => 'boolean',
|
||||
];
|
||||
}
|
||||
@@ -59,9 +81,23 @@ class ExternalStorageSettings extends Model
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->active
|
||||
&& $this->key !== null && $this->key !== ''
|
||||
&& $this->secret !== null && $this->secret !== ''
|
||||
&& $this->bucket !== null && $this->bucket !== '';
|
||||
if (! $this->active || ! $this->filled('bucket')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// What counts as "filled in" is per provider, because the two
|
||||
// authenticate with different things entirely: S3 wants a key and
|
||||
// a secret, GCS wants a service account key file.
|
||||
return match ($this->provider) {
|
||||
StorageProvider::S3 => $this->filled('key') && $this->filled('secret'),
|
||||
StorageProvider::Gcs => $this->filled('key_file'),
|
||||
};
|
||||
}
|
||||
|
||||
private function filled(string $attribute): bool
|
||||
{
|
||||
$value = $this->{$attribute};
|
||||
|
||||
return is_string($value) && $value !== '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ enum Setting: string
|
||||
// Days a denied membership request blocks re-requesting (0 = none).
|
||||
case ClientsMembershipDenyCooldownDays = 'clients_membership_deny_cooldown_days';
|
||||
|
||||
// Whether the client portal offers inline preview at all — the whole
|
||||
// affordance, images included, not just the media types. Staff are
|
||||
// never gated by it: it exists so an installation can decide that a
|
||||
// client either downloads a file or does not get it, without taking
|
||||
// the tool away from the people who administer the library. See
|
||||
// PreviewKind and FileThumbnailController::preview.
|
||||
case ClientsCanPreviewFiles = 'clients_can_preview_files';
|
||||
|
||||
// Maximum upload size in MB (0 = unlimited).
|
||||
case MaxFileSizeMb = 'max_file_size_mb';
|
||||
|
||||
@@ -110,6 +118,18 @@ enum Setting: string
|
||||
// the api_request_logs migration for why the two are separate.
|
||||
case ApiRequestLogRetentionDays = 'api_request_log_retention_days';
|
||||
|
||||
// How long a permanently failed queue job is kept, and how long a
|
||||
// notification somebody has already read is kept. Both tables grow
|
||||
// with use and neither ever shrank on its own: the failed-jobs list
|
||||
// waited for somebody to press "Delete all failed", and nothing at
|
||||
// all pruned notifications. An installation nobody tends should not
|
||||
// pay for that with its database.
|
||||
//
|
||||
// 0 means keep indefinitely in both cases, the same explicit choice
|
||||
// ApiRequestLogRetentionDays offers.
|
||||
case FailedJobRetentionDays = 'failed_job_retention_days';
|
||||
case NotificationRetentionDays = 'notification_retention_days';
|
||||
|
||||
// How many days a self-deleted account is retained (soft-deleted)
|
||||
// before PurgeErasuresCommand permanently erases it. Consumed by
|
||||
// ProfileController.
|
||||
@@ -157,6 +177,12 @@ enum Setting: string
|
||||
// group/file public flags instead of tokens. See PublicGroupsController.
|
||||
case PublicListingEnabled = 'public_listing_enabled';
|
||||
|
||||
// The same switch as ClientsCanPreviewFiles, for the anonymous side:
|
||||
// whether a public file page offers to show the file as well as hand
|
||||
// it over. v1 parity: public_listing_enable_preview. See
|
||||
// PublicGroupsController::preview.
|
||||
case PublicListingPreviewEnabled = 'public_listing_preview_enabled';
|
||||
|
||||
// The configurable base URL segment for the public listing (e.g.
|
||||
// "public" -> /public, /public/{group-slug}). Consumed by
|
||||
// PublicGroupsController's guard against every request's first path
|
||||
@@ -232,6 +258,43 @@ enum Setting: string
|
||||
case LatestReleaseUrl = 'latest_release_url';
|
||||
case LatestReleasePublishedAt = 'latest_release_published_at';
|
||||
|
||||
// The version `projectsend:update` last brought this database in line
|
||||
// with — written by that command only, never by a settings form.
|
||||
//
|
||||
// Its whole purpose is to be compared against config('projectsend.version')
|
||||
// as read by the *web* process, which is not the same number when a
|
||||
// manual install replaced its files and never reloaded PHP-FPM: OPcache
|
||||
// keeps serving the code it compiled before the update, silently, while
|
||||
// artisan reports the new version to the person trying to work out why.
|
||||
// See RunningCodeState.
|
||||
//
|
||||
// Written downwards as well as upwards, because the command runs on
|
||||
// every container boot and on every update.sh run — including the ones
|
||||
// that go backwards.
|
||||
case AppliedVersion = 'applied_version';
|
||||
case AppliedVersionAt = 'applied_version_at';
|
||||
|
||||
// An update finished and the administrator has not been shown what it
|
||||
// brought yet — the two versions it moved between, so the page can
|
||||
// name every release in the gap rather than only the newest.
|
||||
//
|
||||
// Written by UpdateInstallation, and only for a genuine update of an
|
||||
// existing installation: a fresh install has nothing to catch up on,
|
||||
// and a container that reboots has not updated anything. Cleared when
|
||||
// the page is read. Both empty means nothing is waiting.
|
||||
case UpdateWelcomeFrom = 'update_welcome_from';
|
||||
case UpdateWelcomeTo = 'update_welcome_to';
|
||||
|
||||
// This installation has just been installed and its administrator has
|
||||
// not been shown the getting-started page yet.
|
||||
//
|
||||
// Raised where the first administrator is created — the setup screen
|
||||
// and the provisioning command both — and cleared when the page is
|
||||
// read. False by default, which is what keeps an installation that
|
||||
// updates into this feature from being congratulated on an
|
||||
// installation it completed a year ago.
|
||||
case GettingStartedPending = 'getting_started_pending';
|
||||
|
||||
// Cached result of the last dashboard news feed fetch — never written
|
||||
// directly by a settings form, only by FetchNewsCommand. Both editions
|
||||
// see this (unlike CheckForUpdates above, which is Community-only);
|
||||
@@ -280,6 +343,10 @@ enum Setting: string
|
||||
self::LatestReleaseNotes,
|
||||
self::LatestReleaseUrl,
|
||||
self::LatestReleasePublishedAt,
|
||||
self::AppliedVersion,
|
||||
self::AppliedVersionAt,
|
||||
self::UpdateWelcomeFrom,
|
||||
self::UpdateWelcomeTo,
|
||||
self::NewsLastFetchedAt,
|
||||
self::CaptchaProvider,
|
||||
self::CaptchaKeySource,
|
||||
@@ -290,6 +357,8 @@ enum Setting: string
|
||||
self::EmailNotificationsEnabled,
|
||||
self::DiscourageSearchIndexing,
|
||||
self::PublicListingEnabled,
|
||||
self::ClientsCanPreviewFiles,
|
||||
self::PublicListingPreviewEnabled,
|
||||
self::CheckForUpdates,
|
||||
self::ExpiredFilesAutoDeleteEnabled,
|
||||
self::PublicCommentsEnabled,
|
||||
@@ -299,7 +368,8 @@ enum Setting: string
|
||||
self::CaptchaOnRegistration,
|
||||
self::CaptchaOnPasswordReset,
|
||||
self::CaptchaOnPublicComments,
|
||||
self::PasswordRejectBreached => SettingType::Boolean,
|
||||
self::PasswordRejectBreached,
|
||||
self::GettingStartedPending => SettingType::Boolean,
|
||||
|
||||
self::ClientsAutoGroup,
|
||||
self::ClientsMembershipDenyCooldownDays,
|
||||
@@ -308,6 +378,8 @@ enum Setting: string
|
||||
self::AccountErasureGraceDays,
|
||||
self::AccountErasureReassignTo,
|
||||
self::ApiRequestLogRetentionDays,
|
||||
self::FailedJobRetentionDays,
|
||||
self::NotificationRetentionDays,
|
||||
self::ExpiredFilesDeleteAfterDays,
|
||||
self::CommentsEditWindowMinutes,
|
||||
self::OrphanFilesDeleteAfterDays,
|
||||
@@ -354,6 +426,11 @@ enum Setting: string
|
||||
|
||||
self::CheckForUpdates,
|
||||
self::CommentsGuestModeration,
|
||||
// On, so that an installation updating into these switches
|
||||
// keeps the preview it already had rather than losing it to a
|
||||
// setting nobody has seen yet.
|
||||
self::ClientsCanPreviewFiles,
|
||||
self::PublicListingPreviewEnabled,
|
||||
// On by default, but only ever consulted once a provider is
|
||||
// configured — so a fresh install is not protecting forms it
|
||||
// has no keys for.
|
||||
@@ -369,6 +446,8 @@ enum Setting: string
|
||||
self::AccountErasureGraceDays => 30,
|
||||
self::AccountErasureReassignTo => 0,
|
||||
self::ApiRequestLogRetentionDays => 30,
|
||||
self::FailedJobRetentionDays => 30,
|
||||
self::NotificationRetentionDays => 90,
|
||||
self::ExpiredFilesDeleteAfterDays => 30,
|
||||
self::OrphanFilesDeleteAfterDays => 30,
|
||||
self::CommentsEditWindowMinutes => 15,
|
||||
@@ -402,6 +481,17 @@ enum Setting: string
|
||||
self::LatestReleaseNotes => '',
|
||||
self::LatestReleaseUrl => '',
|
||||
self::LatestReleasePublishedAt => '',
|
||||
// Empty means no update has ever been applied through the
|
||||
// command — a fresh install, or one that predates it. Never a
|
||||
// notice, in either direction.
|
||||
self::AppliedVersion => '',
|
||||
self::AppliedVersionAt => '',
|
||||
self::UpdateWelcomeFrom => '',
|
||||
self::UpdateWelcomeTo => '',
|
||||
// False, so that an installation which updates into this
|
||||
// feature is not welcomed to an installation it finished a
|
||||
// year ago. Only creating the first administrator raises it.
|
||||
self::GettingStartedPending => false,
|
||||
self::NewsLastFetchedAt => '',
|
||||
|
||||
self::NewsItems => [],
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Settings;
|
||||
|
||||
/**
|
||||
* Which object store the external `files_external` disk is talking to.
|
||||
*
|
||||
* Unlike MailProvider, this is not a preset picker over one transport:
|
||||
* the two cases are genuinely different Flysystem drivers, authenticated
|
||||
* differently — an access key and secret against an S3 API, a service
|
||||
* account key against Google's. Which fields the Storage settings screen
|
||||
* shows, which of them are validated, and what
|
||||
* ExternalStorageConfigApplier writes into the disk config all follow
|
||||
* from this.
|
||||
*
|
||||
* S3 keeps its endpoint and path-style settings because "S3" here means
|
||||
* the whole S3-compatible family — AWS itself, MinIO, Backblaze,
|
||||
* Wasabi, and Google's own interoperability endpoint for anyone who
|
||||
* would rather use HMAC keys than a service account.
|
||||
*/
|
||||
enum StorageProvider: string
|
||||
{
|
||||
case S3 = 's3';
|
||||
case Gcs = 'gcs';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::S3 => 'S3-compatible',
|
||||
self::Gcs => 'Google Cloud Storage',
|
||||
};
|
||||
}
|
||||
|
||||
/** The Laravel filesystem driver this provider is served by. */
|
||||
public function driver(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::S3 => 's3',
|
||||
self::Gcs => 'gcs',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Storage;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Google\Cloud\Storage\StorageClient;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Flysystem\Config;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\GoogleCloudStorage\GoogleCloudStorageAdapter;
|
||||
use League\Flysystem\GoogleCloudStorage\UniformBucketLevelAccessVisibility;
|
||||
|
||||
/**
|
||||
* The `gcs` filesystem driver, which Laravel does not ship.
|
||||
*
|
||||
* Three things here are not boilerplate, and all of them are the kind
|
||||
* that fail quietly rather than loudly.
|
||||
*
|
||||
* **Laravel will not find the adapter's own method.** FilesystemAdapter
|
||||
* ::temporaryUrl() looks for a method named `getTemporaryUrl` on the
|
||||
* adapter, falls back to a registered callback, and otherwise throws
|
||||
* "This driver does not support creating temporary URLs". League's
|
||||
* adapter implements Flysystem's TemporaryUrlGenerator and names the
|
||||
* method `temporaryUrl`. The names do not meet, so without the
|
||||
* buildTemporaryUrlsUsing() below every download and every preview of a
|
||||
* GCS-stored file is a 500.
|
||||
*
|
||||
* **The default visibility handler cannot write to a correctly
|
||||
* configured bucket.** See the constructor argument below: it attaches a
|
||||
* legacy ACL to every object, and uniform bucket-level access — which the
|
||||
* setup instructions require — rejects the write outright. Combined with
|
||||
* `'throw' => false` on the disk, that failure was completely silent.
|
||||
*
|
||||
* **The two SDKs spell the signing options differently.** The callers —
|
||||
* StoredFileResponse, and anything else that hands options to
|
||||
* temporaryUrl() — speak the AWS vocabulary, because S3 came first and
|
||||
* one vocabulary is better than two. GCS wants `responseDisposition`
|
||||
* where S3 says `ResponseContentDisposition`, and an option it does not
|
||||
* recognise is ignored in silence: no exception, just downloads that
|
||||
* arrive named after the storage key and previews that download instead
|
||||
* of displaying. Translating here is what keeps every caller
|
||||
* provider-agnostic, and keeps the failure from being invisible.
|
||||
*/
|
||||
class GoogleCloudStorageDriver
|
||||
{
|
||||
/**
|
||||
* AWS option name => Google option name, for the subset this
|
||||
* application actually sends. Anything absent is passed through
|
||||
* untouched, so a caller can still reach a Google-specific option by
|
||||
* its real name.
|
||||
*/
|
||||
private const OPTION_NAMES = [
|
||||
'ResponseContentDisposition' => 'responseDisposition',
|
||||
'ResponseContentType' => 'responseType',
|
||||
];
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
Storage::extend('gcs', fn ($app, array $config): FilesystemAdapter => $this->make($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function make(array $config): FilesystemAdapter
|
||||
{
|
||||
$client = new StorageClient(array_filter([
|
||||
// The key file carries its own project_id, so there is
|
||||
// nothing else to configure. Absent, the client falls back to
|
||||
// Application Default Credentials — which is how a self-hosted
|
||||
// install on a Google VM can work with no key at all, at the
|
||||
// cost of an IAM round trip per signature.
|
||||
'keyFile' => is_array($config['key_file'] ?? null) ? $config['key_file'] : null,
|
||||
]));
|
||||
|
||||
$adapter = new GoogleCloudStorageAdapter(
|
||||
$client->bucket((string) ($config['bucket'] ?? '')),
|
||||
(string) ($config['prefix'] ?? ''),
|
||||
// Never write a per-object ACL. The adapter's default handler
|
||||
// attaches one to every object, and a bucket with uniform
|
||||
// bucket-level access turned on rejects the whole write:
|
||||
// "Cannot insert legacy ACL for an object when uniform
|
||||
// bucket-level access is enabled". Uniform access is what the
|
||||
// setup instructions ask for and what Google recommends, so
|
||||
// the default handler fails on a correctly configured bucket.
|
||||
//
|
||||
// Nothing is lost by never setting one. Every object this
|
||||
// application stores is private and every read of it is a
|
||||
// signed URL, so per-object visibility has nothing to say
|
||||
// here — and on a bucket *without* uniform access, an object
|
||||
// written with no ACL simply inherits the bucket's defaults,
|
||||
// which is the same answer.
|
||||
new UniformBucketLevelAccessVisibility,
|
||||
);
|
||||
|
||||
$disk = new FilesystemAdapter(new Filesystem($adapter), $adapter, $config);
|
||||
|
||||
// Bound and captured before registering, not called as
|
||||
// $this->signingOptions() inside the closure: Laravel re-binds the
|
||||
// callback to the FilesystemAdapter before invoking it
|
||||
// (bindTo($this, static::class)), so `$this` in there is the disk,
|
||||
// not this class, and the call fails at the first download rather
|
||||
// than here.
|
||||
$signingOptions = $this->signingOptions(...);
|
||||
|
||||
$disk->buildTemporaryUrlsUsing(
|
||||
fn (string $path, DateTimeInterface $expiration, array $options): string => $adapter->temporaryUrl(
|
||||
$path,
|
||||
$expiration,
|
||||
new Config(['gcp_signing_options' => $signingOptions($options)]),
|
||||
)
|
||||
);
|
||||
|
||||
return $disk;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function signingOptions(array $options): array
|
||||
{
|
||||
$translated = [];
|
||||
|
||||
foreach ($options as $name => $value) {
|
||||
$translated[self::OPTION_NAMES[$name] ?? $name] = $value;
|
||||
}
|
||||
|
||||
// V4 explicitly rather than by default: v2 signatures are the
|
||||
// library's historical default in some paths, they are deprecated,
|
||||
// and the difference only shows up as a rejected URL at the moment
|
||||
// somebody tries to download something.
|
||||
return ['version' => 'v4', ...$translated];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Updates;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Identity\Permissions\Permission;
|
||||
use App\Modules\Identity\Permissions\PermissionChecker;
|
||||
use App\Modules\Identity\UserType;
|
||||
use App\Modules\Notifications\Notifier;
|
||||
use App\Modules\Platform\Capabilities\Capability;
|
||||
use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Ask the public repository what the newest release is, and remember the
|
||||
* answer.
|
||||
*
|
||||
* There is no in-app self-updater and this is not one: it reads a feed
|
||||
* and writes settings. Applying an update is always somebody running
|
||||
* `update.sh` or pulling a new image.
|
||||
*
|
||||
* This lives in a class rather than in the command because there are two
|
||||
* callers now — the daily scheduled run and the button in the settings —
|
||||
* and the part that must not drift between them is the part with
|
||||
* consequences: which staff get notified, and the guard that stops them
|
||||
* being notified twice for the same release. A second copy of that in a
|
||||
* controller would be found wrong six months later, by a staff member
|
||||
* receiving the same notification every time somebody pressed a button.
|
||||
*/
|
||||
class CheckForUpdates
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CapabilityRegistry $capabilities,
|
||||
private readonly Settings $settings,
|
||||
private readonly PermissionChecker $permissions,
|
||||
private readonly Notifier $notifier,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{ok: bool, outcome: 'unavailable'|'unreachable'|'unrecognised'|'checked', message: string, latest_version: string, update_available: bool}
|
||||
*/
|
||||
public function run(): array
|
||||
{
|
||||
if (! $this->capabilities->has(Capability::SystemUpdates)) {
|
||||
return $this->result(true, 'unavailable', __('Update checks are not available on this edition.'));
|
||||
}
|
||||
|
||||
$response = Http::withHeaders(['User-Agent' => 'ProjectSend'])
|
||||
->timeout(10)
|
||||
->get('https://api.github.com/repos/projectsend/projectsend/releases/latest');
|
||||
|
||||
if (! $response->successful()) {
|
||||
return $this->result(false, 'unreachable', __('Could not reach the update feed.'));
|
||||
}
|
||||
|
||||
$tag = (string) ($response->json('tag_name') ?? '');
|
||||
$latestVersion = ltrim($tag, 'v');
|
||||
|
||||
// The public repo's tags are still v1's r-number scheme (r2029,
|
||||
// not SemVer) until a real Community v2 release ships there —
|
||||
// skip rather than misreport an "update" against a tag we can't
|
||||
// meaningfully compare.
|
||||
if (! preg_match('/^\d+\.\d+\.\d+/', $latestVersion)) {
|
||||
return $this->result(true, 'unrecognised', __('The latest release (:tag) is not a version this can compare.', ['tag' => $tag]));
|
||||
}
|
||||
|
||||
$currentVersion = (string) config('projectsend.version');
|
||||
$previouslyKnownVersion = $this->settings->get(Setting::LatestKnownVersion);
|
||||
|
||||
$this->settings->set(Setting::LatestKnownVersion, $latestVersion);
|
||||
$this->settings->set(Setting::LatestVersionCheckedAt, now()->toIso8601String());
|
||||
$this->settings->set(Setting::LatestReleaseTitle, (string) ($response->json('name') ?? $tag));
|
||||
$this->settings->set(Setting::LatestReleaseNotes, (string) ($response->json('body') ?? ''));
|
||||
$this->settings->set(Setting::LatestReleaseUrl, (string) ($response->json('html_url') ?? ''));
|
||||
$this->settings->set(Setting::LatestReleasePublishedAt, (string) ($response->json('published_at') ?? ''));
|
||||
|
||||
$updateAvailable = version_compare($latestVersion, $currentVersion, '>');
|
||||
$alreadyKnown = $previouslyKnownVersion === $latestVersion;
|
||||
|
||||
// Only the first time a genuinely newer release is seen. This is
|
||||
// what keeps the settings button from notifying every staff member
|
||||
// again on every press, and it belongs here rather than in either
|
||||
// caller for exactly that reason.
|
||||
if ($updateAvailable && ! $alreadyKnown) {
|
||||
$recipients = array_values(User::query()->where('type', UserType::Staff)->get()
|
||||
->filter(fn (User $staff): bool => $this->permissions->allows($staff, Permission::ManageUpdates))
|
||||
->all());
|
||||
|
||||
$this->notifier->send('update_available', $recipients, data: [
|
||||
'latestVersion' => $latestVersion,
|
||||
'currentVersion' => $currentVersion,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->result(
|
||||
true,
|
||||
'checked',
|
||||
$updateAvailable
|
||||
? __('Version :version is available. You are running :current.', ['version' => $latestVersion, 'current' => $currentVersion])
|
||||
: __('You are up to date, running version :current.', ['current' => $currentVersion]),
|
||||
$latestVersion,
|
||||
$updateAvailable,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'unavailable'|'unreachable'|'unrecognised'|'checked' $outcome
|
||||
* @return array{ok: bool, outcome: 'unavailable'|'unreachable'|'unrecognised'|'checked', message: string, latest_version: string, update_available: bool}
|
||||
*/
|
||||
private function result(bool $ok, string $outcome, string $message, string $latestVersion = '', bool $updateAvailable = false): array
|
||||
{
|
||||
return [
|
||||
'ok' => $ok,
|
||||
'outcome' => $outcome,
|
||||
'message' => $message,
|
||||
'latest_version' => $latestVersion,
|
||||
'update_available' => $updateAvailable,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,26 +4,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Updates\Console;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Identity\Permissions\Permission;
|
||||
use App\Modules\Identity\Permissions\PermissionChecker;
|
||||
use App\Modules\Identity\UserType;
|
||||
use App\Modules\Notifications\Notifier;
|
||||
use App\Modules\Platform\Capabilities\Capability;
|
||||
use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use App\Modules\Platform\Updates\CheckForUpdates;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Community edition only. There is no in-app self-updater — v1's
|
||||
* download-a-zip-and-extract-it-over-the-live-install approach doesn't
|
||||
* make sense once the app runs from a Docker image (see
|
||||
* docs/... — the rewrite brief explicitly rejects porting it). This
|
||||
* command only ever checks the public repo's latest GitHub release and
|
||||
* caches the result; upgrading is always an admin running
|
||||
* `docker compose pull && docker compose up -d` themselves.
|
||||
* Community edition only, once a day. The work itself lives in
|
||||
* CheckForUpdates, which the settings screen's "check now" button calls
|
||||
* too; this is the scheduled half, and the only thing it adds is the
|
||||
* setting that switches the schedule off.
|
||||
*
|
||||
* That setting governs *this* command and not the service on purpose: an
|
||||
* administrator who does not want a daily outbound call should still be
|
||||
* able to ask the question themselves.
|
||||
*
|
||||
* There is still no in-app self-updater — nothing here downloads or
|
||||
* applies anything. Applying an update is `update.sh` on a server
|
||||
* install, or a new image on a container one.
|
||||
*/
|
||||
class CheckForUpdatesCommand extends Command
|
||||
{
|
||||
@@ -32,76 +30,29 @@ class CheckForUpdatesCommand extends Command
|
||||
protected $description = 'Check for a newer ProjectSend release and notify admins (Community edition, runs daily)';
|
||||
|
||||
public function __construct(
|
||||
private readonly CapabilityRegistry $capabilities,
|
||||
private readonly Settings $settings,
|
||||
private readonly PermissionChecker $permissions,
|
||||
private readonly Notifier $notifier,
|
||||
private readonly CheckForUpdates $check,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! $this->capabilities->has(Capability::SystemUpdates)) {
|
||||
$this->info('Update checks are not available on this edition.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($this->settings->get(Setting::CheckForUpdates) !== true) {
|
||||
$this->info('Update checks are disabled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$response = Http::withHeaders(['User-Agent' => 'ProjectSend'])
|
||||
->timeout(10)
|
||||
->get('https://api.github.com/repos/projectsend/projectsend/releases/latest');
|
||||
$result = $this->check->run();
|
||||
|
||||
if (! $response->successful()) {
|
||||
$this->warn('Could not reach the update feed.');
|
||||
if (! $result['ok']) {
|
||||
$this->warn($result['message']);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$tag = (string) ($response->json('tag_name') ?? '');
|
||||
$latestVersion = ltrim($tag, 'v');
|
||||
|
||||
// The public repo's tags are still v1's r-number scheme (r2029,
|
||||
// not SemVer) until a real Community v2 release ships there —
|
||||
// skip rather than misreport an "update" against a tag we can't
|
||||
// meaningfully compare.
|
||||
if (! preg_match('/^\d+\.\d+\.\d+/', $latestVersion)) {
|
||||
$this->info("Latest release tag ({$tag}) is not a recognized version — skipping.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$currentVersion = (string) config('projectsend.version');
|
||||
$previouslyKnownVersion = $this->settings->get(Setting::LatestKnownVersion);
|
||||
|
||||
$this->settings->set(Setting::LatestKnownVersion, $latestVersion);
|
||||
$this->settings->set(Setting::LatestVersionCheckedAt, now()->toIso8601String());
|
||||
$this->settings->set(Setting::LatestReleaseTitle, (string) ($response->json('name') ?? $tag));
|
||||
$this->settings->set(Setting::LatestReleaseNotes, (string) ($response->json('body') ?? ''));
|
||||
$this->settings->set(Setting::LatestReleaseUrl, (string) ($response->json('html_url') ?? ''));
|
||||
$this->settings->set(Setting::LatestReleasePublishedAt, (string) ($response->json('published_at') ?? ''));
|
||||
|
||||
$updateAvailable = version_compare($latestVersion, $currentVersion, '>');
|
||||
$alreadyKnown = $previouslyKnownVersion === $latestVersion;
|
||||
|
||||
if ($updateAvailable && ! $alreadyKnown) {
|
||||
$recipients = array_values(User::query()->where('type', UserType::Staff)->get()
|
||||
->filter(fn (User $staff): bool => $this->permissions->allows($staff, Permission::ManageUpdates))
|
||||
->all());
|
||||
|
||||
$this->notifier->send('update_available', $recipients, data: [
|
||||
'latestVersion' => $latestVersion,
|
||||
'currentVersion' => $currentVersion,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->info("Latest version: {$latestVersion} (current: {$currentVersion}).");
|
||||
$this->info($result['message']);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Updates\Console;
|
||||
|
||||
use App\Modules\Platform\Installation\Installation;
|
||||
use App\Modules\Platform\Installation\InstallationKind;
|
||||
use App\Modules\Platform\Updates\UpdateInstallation;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Bring this installation in line with the code that is on disk.
|
||||
*
|
||||
* No options, deliberately: the container entrypoints, update.sh and an
|
||||
* administrator typing it all invoke the identical string, which is what
|
||||
* makes "one definition of an update" true rather than aspirational.
|
||||
* UpdateInstallation holds the sequence and the reasoning.
|
||||
*/
|
||||
class UpdateCommand extends Command
|
||||
{
|
||||
protected $signature = 'projectsend:update';
|
||||
|
||||
protected $description = 'Bring the database, roles and caches in line with the installed code (idempotent; run on every boot)';
|
||||
|
||||
public function handle(UpdateInstallation $update, Installation $installation): int
|
||||
{
|
||||
$result = $update->run($this->output);
|
||||
|
||||
if (! $result['ok']) {
|
||||
foreach ($result['warnings'] as $warning) {
|
||||
$this->error($warning);
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('System roles are in place.');
|
||||
|
||||
if ($result['cleared'] !== []) {
|
||||
$this->info('Cleared the compiled configuration, events, routes and views.');
|
||||
}
|
||||
|
||||
if ($result['rewarmed'] !== []) {
|
||||
$this->info('Rebuilt the route, event and view caches — they were in place before.');
|
||||
}
|
||||
|
||||
$this->info('Asked the background workers to restart.');
|
||||
|
||||
foreach ($result['warnings'] as $warning) {
|
||||
$this->warn($warning);
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info($this->summary($result['from'], $result['to']));
|
||||
|
||||
// A container is replaced rather than reloaded, and its operator
|
||||
// has no systemd to reload anything with — printing this there
|
||||
// would send them looking for a service that does not exist.
|
||||
if ($installation->kind() === InstallationKind::Manual) {
|
||||
$this->newLine();
|
||||
$this->warn('The web server is still running the code it compiled before this ran.');
|
||||
$this->warn('Reload PHP-FPM now, or it will keep serving it: sudo systemctl reload php8.4-fpm');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function summary(string $from, string $to): string
|
||||
{
|
||||
if ($from === '') {
|
||||
return "Applied {$to}.";
|
||||
}
|
||||
|
||||
if ($from === $to) {
|
||||
return "Re-applied {$to}.";
|
||||
}
|
||||
|
||||
return "Updated from {$from} to {$to}.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Updates;
|
||||
|
||||
/**
|
||||
* Reads CHANGELOG.md and hands back the releases in a version range, as
|
||||
* structure rather than as Markdown.
|
||||
*
|
||||
* The changelog is the source because it is the only description of a
|
||||
* release that is *inside* the release. GitHub's release notes would need
|
||||
* a network call at the exact moment an administrator has just updated —
|
||||
* possibly on a server with no outbound access at all — to describe code
|
||||
* that is already sitting on disk. This file ships in every zip
|
||||
* (verify-release.sh refuses one without it) and therefore in the image
|
||||
* built from it.
|
||||
*
|
||||
* Parsed rather than rendered: nothing here ever becomes HTML. The page
|
||||
* gets titles and sentences and does its own formatting, so a stray
|
||||
* `<script>` in a changelog written years from now cannot become one.
|
||||
*/
|
||||
class ReleaseNotes
|
||||
{
|
||||
/**
|
||||
* `## 2.1.0 — 2026-08-20`, with the date optional. The separator is an
|
||||
* em dash in every entry written so far; a hyphen is accepted too,
|
||||
* since which one gets typed is not something to lose a release note
|
||||
* over.
|
||||
*/
|
||||
private const HEADING = '/^##\s+v?(?<version>\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*(?:[—–-]\s*(?<date>.+?))?\s*$/u';
|
||||
|
||||
/**
|
||||
* Every release later than $from, up to and including $to, newest
|
||||
* first.
|
||||
*
|
||||
* An empty $from means the installation never recorded a version, so
|
||||
* there is no gap to describe and only $to is shown. Versions are
|
||||
* compared, not string-matched: a changelog holds every release ever
|
||||
* made, and only the ones this update actually crossed are news.
|
||||
*
|
||||
* @return list<array{version: string, date: string, intro: list<string>, groups: list<array{heading: string, items: list<array{title: string, body: string}>}>}>
|
||||
*/
|
||||
public function between(string $from, string $to, ?string $changelog = null): array
|
||||
{
|
||||
if ($to === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(
|
||||
$this->all($changelog),
|
||||
fn (array $release): bool => version_compare($release['version'], $to, '<=')
|
||||
&& ($from === '' ? $release['version'] === $to : version_compare($release['version'], $from, '>')),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every release in the changelog, newest first, in file order.
|
||||
*
|
||||
* "Unreleased" is skipped by construction: its heading carries no
|
||||
* version number, so it never matches.
|
||||
*
|
||||
* @return list<array{version: string, date: string, intro: list<string>, groups: list<array{heading: string, items: list<array{title: string, body: string}>}>}>
|
||||
*/
|
||||
public function all(?string $changelog = null): array
|
||||
{
|
||||
$markdown = $changelog ?? $this->read();
|
||||
|
||||
if ($markdown === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$releases = [];
|
||||
$current = null;
|
||||
|
||||
foreach ($this->paragraphs($markdown) as $block) {
|
||||
if (preg_match(self::HEADING, $block, $matches) === 1) {
|
||||
if ($current !== null) {
|
||||
$releases[] = $current;
|
||||
}
|
||||
|
||||
$current = [
|
||||
'version' => $matches['version'],
|
||||
'date' => trim($matches['date'] ?? ''),
|
||||
'intro' => [],
|
||||
'groups' => [],
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($current === null) {
|
||||
// The changelog's own preamble, before the first release.
|
||||
continue;
|
||||
}
|
||||
|
||||
$current = $this->absorb($current, $block);
|
||||
}
|
||||
|
||||
if ($current !== null) {
|
||||
$releases[] = $current;
|
||||
}
|
||||
|
||||
return $releases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold one block of the release's body into it.
|
||||
*
|
||||
* @param array{version: string, date: string, intro: list<string>, groups: list<array{heading: string, items: list<array{title: string, body: string}>}>} $release
|
||||
* @return array{version: string, date: string, intro: list<string>, groups: list<array{heading: string, items: list<array{title: string, body: string}>}>}
|
||||
*/
|
||||
private function absorb(array $release, string $block): array
|
||||
{
|
||||
if (preg_match('/^###\s+(?<heading>.+?)\s*$/u', $block, $matches) === 1) {
|
||||
$release['groups'][] = ['heading' => $matches['heading'], 'items' => []];
|
||||
|
||||
return $release;
|
||||
}
|
||||
|
||||
if (! str_starts_with(ltrim($block), '- ')) {
|
||||
// Prose. Before the first "### Added" it is the release's own
|
||||
// summary — 2.0.0 opens with three paragraphs of it. After
|
||||
// one, it belongs to that group and is rare enough that
|
||||
// folding it in as an untitled item beats inventing a shape
|
||||
// for it.
|
||||
if ($release['groups'] === []) {
|
||||
$release['intro'][] = $this->inline($block);
|
||||
|
||||
return $release;
|
||||
}
|
||||
|
||||
return $this->append($release, ['title' => '', 'body' => $this->inline($block)]);
|
||||
}
|
||||
|
||||
foreach ($this->bullets($block) as $bullet) {
|
||||
$release = $this->append($release, $this->item($bullet));
|
||||
}
|
||||
|
||||
return $release;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the release's last group, opening an unnamed one if
|
||||
* there is nothing to add it to — the shape a small fix-only release
|
||||
* takes when it lists changes without a `### Heading` above them.
|
||||
*
|
||||
* @param array{version: string, date: string, intro: list<string>, groups: list<array{heading: string, items: list<array{title: string, body: string}>}>} $release
|
||||
* @param array{title: string, body: string} $item
|
||||
* @return array{version: string, date: string, intro: list<string>, groups: list<array{heading: string, items: list<array{title: string, body: string}>}>}
|
||||
*/
|
||||
private function append(array $release, array $item): array
|
||||
{
|
||||
if ($release['groups'] === []) {
|
||||
$release['groups'][] = ['heading' => '', 'items' => []];
|
||||
}
|
||||
|
||||
$index = count($release['groups']) - 1;
|
||||
$group = $release['groups'][$index];
|
||||
$group['items'][] = $item;
|
||||
$release['groups'][$index] = $group;
|
||||
|
||||
return $release;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a bullet into its bold lead-in and the rest.
|
||||
*
|
||||
* Entries are written as `- **What changed.** Why it matters`, and
|
||||
* that lead-in is the only part most people read, so it becomes a
|
||||
* heading on screen.
|
||||
*
|
||||
* The sentence-ending punctuation *inside* the bold is what makes it a
|
||||
* lead-in. Some bullets instead open with a bold fragment that the
|
||||
* sentence continues through — `- **Clients sign in with their email
|
||||
* address**, not their username` — and promoting that to a heading
|
||||
* produces "Clients sign in with their email address. , not their
|
||||
* username". Those keep their sentence whole and get no title.
|
||||
*
|
||||
* @return array{title: string, body: string}
|
||||
*/
|
||||
private function item(string $bullet): array
|
||||
{
|
||||
if (preg_match('/^\*\*(?<title>.+?)\*\*(?<body>.*)$/us', $bullet, $matches) === 1) {
|
||||
$title = $this->inline($matches['title']);
|
||||
|
||||
if (preg_match('/[.!?]$/u', $title) === 1) {
|
||||
return [
|
||||
// The full stop belongs to the sentence, not to the
|
||||
// heading it becomes on screen.
|
||||
'title' => rtrim($title, '.'),
|
||||
'body' => $this->inline($matches['body']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['title' => '', 'body' => $this->inline($bullet)];
|
||||
}
|
||||
|
||||
/**
|
||||
* The bullets in one list block, each with its continuation lines
|
||||
* joined back on. Markdown wraps at 100 columns here; a reader wants
|
||||
* the sentence.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function bullets(string $block): array
|
||||
{
|
||||
$bullets = [];
|
||||
|
||||
foreach (explode("\n", $block) as $line) {
|
||||
if (preg_match('/^\s*[-*]\s+(?<text>.*)$/u', $line, $matches) === 1) {
|
||||
$bullets[] = $matches['text'];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($bullets !== []) {
|
||||
$bullets[array_key_last($bullets)] .= ' '.trim($line);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(trim(...), $bullets), fn (string $b): bool => $b !== ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown down to text, for everything that is not the bold lead-in:
|
||||
* emphasis and code fences go, links keep their text.
|
||||
*
|
||||
* Kept deliberately small. This renders our own changelog, not
|
||||
* arbitrary Markdown, and the failure mode of a light touch is a
|
||||
* stray asterisk rather than a mangled sentence.
|
||||
*/
|
||||
private function inline(string $text): string
|
||||
{
|
||||
$text = (string) preg_replace('/\[(?<text>[^]]+)]\([^)]+\)/u', '$1', $text);
|
||||
$text = (string) preg_replace('/\*\*(?<text>.+?)\*\*/us', '$1', $text);
|
||||
$text = (string) preg_replace('/`(?<text>[^`]+)`/u', '$1', $text);
|
||||
|
||||
return trim((string) preg_replace('/\s+/u', ' ', $text));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function paragraphs(string $markdown): array
|
||||
{
|
||||
$blocks = preg_split('/\n\s*\n/u', str_replace("\r\n", "\n", $markdown)) ?: [];
|
||||
|
||||
return array_values(array_filter(array_map(trim(...), $blocks), fn (string $b): bool => $b !== ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Null when there is no changelog to read — which is not a failure
|
||||
* worth reporting: a development checkout may be mid-rewrite, and the
|
||||
* page it feeds says thank you either way.
|
||||
*/
|
||||
private function read(): ?string
|
||||
{
|
||||
$path = base_path('CHANGELOG.md');
|
||||
|
||||
if (! is_file($path) || ! is_readable($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$contents = file_get_contents($path);
|
||||
|
||||
return $contents === false ? null : $contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Updates;
|
||||
|
||||
use App\Modules\Platform\Installation\Installation;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
|
||||
/**
|
||||
* Whether the code this process is executing is the code the installation
|
||||
* was last brought in line with.
|
||||
*
|
||||
* It exists because of a failure with no symptom. On a server with OPcache
|
||||
* set the way production guides recommend — `validate_timestamps=0`, which
|
||||
* this project's own image ships — replacing the files changes nothing for
|
||||
* the running site: PHP never re-reads a file it has already compiled. The
|
||||
* database ends up on the new version while every visitor is served the
|
||||
* old code, and `php artisan` reports the new version throughout, because
|
||||
* a fresh CLI process compiles the new files. Nothing errors. The only
|
||||
* outward sign is behaviour that quietly does not match the release notes.
|
||||
*
|
||||
* `projectsend:update` records the version it applied, and this compares
|
||||
* that against what *this* process compiled. The two answers differ in
|
||||
* both directions, and both are worth saying:
|
||||
*
|
||||
* applied > running — files were updated, PHP was never reloaded.
|
||||
* applied < running — new files are in place and the update never ran,
|
||||
* so the schema is behind the code.
|
||||
*
|
||||
* Not gated on an edition. `manage_updates` maps to a Community-only
|
||||
* capability, but what code a server is executing is not a feature — it is
|
||||
* a fact about the machine, which is what view_system_info governs.
|
||||
*/
|
||||
class RunningCodeState
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Settings $settings,
|
||||
private readonly Installation $installation,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{reason: string, applied: string, running: string, applied_at: string, install_kind: string}|null
|
||||
*/
|
||||
public function current(): ?array
|
||||
{
|
||||
$applied = $this->settings->get(Setting::AppliedVersion);
|
||||
$running = (string) config('projectsend.version');
|
||||
|
||||
// No update has ever been applied through the command: a fresh
|
||||
// install, or one older than the command itself. Nothing to say in
|
||||
// either direction.
|
||||
if (! is_string($applied) || $applied === '' || $running === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reason = match (true) {
|
||||
version_compare($applied, $running, '>') => 'stale_code',
|
||||
version_compare($applied, $running, '<') => 'pending_update',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($reason === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$appliedAt = $this->settings->get(Setting::AppliedVersionAt);
|
||||
|
||||
return [
|
||||
'reason' => $reason,
|
||||
'applied' => $applied,
|
||||
'running' => $running,
|
||||
'applied_at' => is_string($appliedAt) ? $appliedAt : '',
|
||||
'install_kind' => $this->installation->kind()->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Updates;
|
||||
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Identity\Permissions\EnsureSystemRoles;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Everything that has to happen after new code lands, and that the
|
||||
* application can do to itself.
|
||||
*
|
||||
* One definition, three callers: the two container entrypoints run it on
|
||||
* every boot, and update.sh runs it after unpacking a release on a server
|
||||
* somebody administers by hand. Before this existed the sequence was
|
||||
* written out in five places — both entrypoints, INSTALL.md, UPDATE.md and
|
||||
* a code block in the dashboard — and they had already drifted: only the
|
||||
* documents mentioned the queue, only the container relinked storage, and
|
||||
* none of them agreed on which caches to clear.
|
||||
*
|
||||
* The order is the design. Each constraint below cost something to find:
|
||||
*
|
||||
* - migrate first, because everything after it assumes tables exist;
|
||||
* - queue:restart last, because it writes its signal *into the cache*,
|
||||
* so anything that clears the cache afterwards deletes the signal and
|
||||
* leaves a worker running the old code forever;
|
||||
* - the caches are cleared one command at a time rather than through
|
||||
* optimize:clear, which also runs cache:clear — and Laravel's Redis
|
||||
* cache store implements that as FLUSHDB. On the default config the
|
||||
* cache sits on its own database and that is harmless; on a managed
|
||||
* Redis offering one database, or any install that pointed cache and
|
||||
* sessions at the same one, an update would sign every user out and
|
||||
* delete the queue. Nothing here needs the data cache dropped:
|
||||
* Settings::set() already forgets its own key, and cached values whose
|
||||
* shape changes get a new key. A human who suspects a stale value has
|
||||
* `php artisan cache:clear`, which UPDATE.md points at.
|
||||
*
|
||||
* What it deliberately does not do is maintenance mode. `php artisan down`
|
||||
* writes storage/framework/maintenance.php, and in the official image
|
||||
* storage/ is the persisted volume — a container that died between `down`
|
||||
* and `up` would come back down, and stay down through every recreation.
|
||||
* update.sh owns maintenance mode, where a trap can guarantee the site
|
||||
* comes back. This is why the command is safe to run on every boot.
|
||||
*/
|
||||
class UpdateInstallation
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Application $app,
|
||||
private readonly EnsureSystemRoles $roles,
|
||||
private readonly Settings $settings,
|
||||
private readonly ActivityLogger $activity,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* from: string,
|
||||
* to: string,
|
||||
* migrated: bool,
|
||||
* cleared: list<string>,
|
||||
* rewarmed: list<string>,
|
||||
* warnings: list<string>,
|
||||
* ok: bool,
|
||||
* }
|
||||
*/
|
||||
public function run(?OutputStyle $output = null): array
|
||||
{
|
||||
$running = (string) config('projectsend.version');
|
||||
|
||||
$result = [
|
||||
'from' => '',
|
||||
'to' => $running,
|
||||
'migrated' => false,
|
||||
'cleared' => [],
|
||||
'rewarmed' => [],
|
||||
'warnings' => [],
|
||||
'ok' => false,
|
||||
];
|
||||
|
||||
// Asked before migrating, because afterwards there is no way to tell
|
||||
// a fresh installation from an existing one — and the difference
|
||||
// decides whether this run is an update worth logging or the first
|
||||
// boot of a brand new install.
|
||||
$existingInstall = $this->hasRunMigrationsBefore();
|
||||
|
||||
// Caught rather than left to surface as a stack trace: the most
|
||||
// likely failure here is a database that is unreachable or refusing
|
||||
// the credentials, and forty frames of Laravel internals is a worse
|
||||
// answer to that than one sentence naming it.
|
||||
try {
|
||||
$migrated = $this->artisan('migrate', ['--force' => true], $output) === 0;
|
||||
} catch (Throwable $exception) {
|
||||
$result['warnings'][] = 'The database migration failed: '.$exception->getMessage();
|
||||
$result['warnings'][] = 'Nothing else was changed.';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (! $migrated) {
|
||||
$result['warnings'][] = 'The database migration failed. Nothing else was changed.';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['migrated'] = true;
|
||||
$result['from'] = $this->previouslyApplied();
|
||||
|
||||
$this->roles->ensure();
|
||||
|
||||
// Not parity with the old entrypoint line — a fix. The release zip
|
||||
// ships no public/storage symlink (the build refuses symlinks
|
||||
// outright, they do not survive zipping), so an installation that
|
||||
// followed UPDATE.md's "unpack beside it and swap the directories"
|
||||
// advice loses the link entirely, and nothing in the documented
|
||||
// sequence ever put it back.
|
||||
$this->artisan('storage:link', ['--force' => true], $output);
|
||||
|
||||
// Read before anything is cleared: this is the only moment the
|
||||
// question "was this installation using the optional caches?" can
|
||||
// still be answered.
|
||||
$warm = $this->warmCaches();
|
||||
|
||||
foreach (['config:clear', 'clear-compiled', 'event:clear', 'route:clear', 'view:clear'] as $command) {
|
||||
if ($this->artisan($command, [], $output) === 0) {
|
||||
$result['cleared'][] = $command;
|
||||
}
|
||||
}
|
||||
|
||||
if ($warm['config']) {
|
||||
$result['warnings'][] = 'A cached configuration was found and cleared. Do not run config:cache on this'
|
||||
.' application — it stops TRUSTED_PROXIES from being read at all. See INSTALL.md.';
|
||||
}
|
||||
|
||||
foreach ($this->cachesToRewarm($warm) as $command) {
|
||||
// A route table that will not compile is a slower site; a
|
||||
// failed update is a broken one. Never fatal.
|
||||
if ($this->artisan($command, [], $output) === 0) {
|
||||
$result['rewarmed'][] = $command;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$result['warnings'][] = "{$command} failed, so that cache is not in place. The site runs without it.";
|
||||
}
|
||||
|
||||
$this->settings->set(Setting::AppliedVersion, $running);
|
||||
$this->settings->set(Setting::AppliedVersionAt, now()->toIso8601String());
|
||||
|
||||
// Everything that happens only when this run actually moved the
|
||||
// installation somewhere new. Both entrypoints run this command on
|
||||
// every boot, so "somewhere new" is the narrow case, not the
|
||||
// common one.
|
||||
if ($existingInstall && $result['from'] !== $running) {
|
||||
$warning = $this->recordTheUpdate($result['from'], $running);
|
||||
|
||||
if ($warning !== null) {
|
||||
$result['warnings'][] = $warning;
|
||||
}
|
||||
|
||||
// Forwards only. Going back is a version change worth recording
|
||||
// above, but somebody who has just restored an older release is
|
||||
// dealing with a problem, and "thank you for keeping this
|
||||
// current" is the wrong thing to greet them with. An unknown
|
||||
// previous version counts as forwards: it is the first update
|
||||
// of an installation older than this feature.
|
||||
if ($result['from'] === '' || version_compare($running, $result['from'], '>')) {
|
||||
$this->raiseTheWelcome($result['from'], $running);
|
||||
}
|
||||
}
|
||||
|
||||
// Last, and after every cache operation above — see the class
|
||||
// docblock. A worker only learns to exit by reading this signal.
|
||||
$this->artisan('queue:restart', [], $output);
|
||||
|
||||
$result['ok'] = true;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave the marker the administrator's welcome page consumes: an
|
||||
* update landed, and this is the ground it covered.
|
||||
*
|
||||
* Both versions rather than just the new one, because an installation
|
||||
* that skipped releases — anybody who updates twice a year — should be
|
||||
* shown every release in the gap and not only the newest.
|
||||
*
|
||||
* Swallows failure for the reason the activity log below does: an
|
||||
* update that worked must not report failure because of a greeting.
|
||||
*/
|
||||
private function raiseTheWelcome(string $from, string $to): void
|
||||
{
|
||||
try {
|
||||
$this->settings->set(Setting::UpdateWelcomeFrom, $from);
|
||||
$this->settings->set(Setting::UpdateWelcomeTo, $to);
|
||||
} catch (Throwable) {
|
||||
// Nothing worth telling anyone: the update itself is complete,
|
||||
// and the only casualty is a page nobody has seen yet.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the update in the activity log — the one place an administrator
|
||||
* looks to answer "what changed on this installation, and when".
|
||||
*
|
||||
* Only reached for a genuine version change (see the caller). The
|
||||
* container entrypoint runs this on every boot, so logging
|
||||
* unconditionally would bury the log under an entry per restart; and a
|
||||
* fresh installation is an install, not an update, which SetupCompleted
|
||||
* already covers.
|
||||
*
|
||||
* "Fresh" is decided by whether migrations had ever run before this
|
||||
* one, rather than by whether a version was recorded: the first update
|
||||
* of any installation older than this command finds no recorded
|
||||
* version, and that update is exactly the one worth logging.
|
||||
*/
|
||||
private function recordTheUpdate(string $from, string $to): ?string
|
||||
{
|
||||
try {
|
||||
$this->activity->logSystem(Action::ApplicationUpdated, [
|
||||
// The previous version is unknown exactly once per
|
||||
// installation: the first update after adopting this command.
|
||||
// A dash rather than a sentence, because context values are
|
||||
// substituted into the template verbatim and never
|
||||
// translated — an English phrase here would sit inside an
|
||||
// otherwise Japanese or Polish row forever.
|
||||
'from' => $from !== '' ? $from : '—',
|
||||
'to' => $to,
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
// An update that worked must not report failure because its own
|
||||
// paperwork did.
|
||||
return 'The update could not be written to the activity log: '.$exception->getMessage();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this database has been migrated before — i.e. whether there
|
||||
* was an installation here at all before this run.
|
||||
*/
|
||||
protected function hasRunMigrationsBefore(): bool
|
||||
{
|
||||
try {
|
||||
return Schema::hasTable('migrations') && DB::table('migrations')->exists();
|
||||
} catch (Throwable) {
|
||||
// No database yet is not an existing installation.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What the previous run of this recorded, or '' when there was none.
|
||||
*
|
||||
* Swallows failures on purpose: by this point the schema is current,
|
||||
* but a cache store that is momentarily unreachable must not fail a
|
||||
* container boot over a line of reporting.
|
||||
*/
|
||||
private function previouslyApplied(): string
|
||||
{
|
||||
try {
|
||||
$applied = $this->settings->get(Setting::AppliedVersion);
|
||||
|
||||
return is_string($applied) ? $applied : '';
|
||||
} catch (Throwable) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Which optional caches this installation had in place.
|
||||
*
|
||||
* Asked through the framework's own path accessors rather than the
|
||||
* filenames: `routes-v7.php` is an internal that changes with major
|
||||
* versions. And by file_exists() rather than $app->routesAreCached(),
|
||||
* which memoises at bootstrap and would still answer true after
|
||||
* route:clear ran in this same process.
|
||||
*
|
||||
* @return array{route: bool, event: bool, config: bool}
|
||||
*/
|
||||
protected function warmCaches(): array
|
||||
{
|
||||
return [
|
||||
'route' => file_exists($this->app->getCachedRoutesPath()),
|
||||
'event' => file_exists($this->app->getCachedEventsPath()),
|
||||
'config' => file_exists($this->app->getCachedConfigPath()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Views have no honest signal of their own — storage/framework/views
|
||||
* fills up from ordinary traffic, cached deliberately or not. So the
|
||||
* route and event caches stand in for the set: their presence means
|
||||
* this installation followed INSTALL.md's "Making it faster", which
|
||||
* lists all three together. A container caches none of them and so
|
||||
* rebuilds nothing, which keeps boot as fast as it is today.
|
||||
*
|
||||
* config:cache is never rebuilt, at any time, for any installation.
|
||||
*
|
||||
* @param array{route: bool, event: bool, config: bool} $warm
|
||||
* @return list<string>
|
||||
*/
|
||||
private function cachesToRewarm(array $warm): array
|
||||
{
|
||||
if (! $warm['route'] && ! $warm['event']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ['route:cache', 'event:cache', 'view:cache'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected so a test can watch the sequence without running it. The
|
||||
* ordering constraints in run() are invisible in its result and
|
||||
* catastrophic when wrong, and an ordered list of calls is the only
|
||||
* thing that can assert them.
|
||||
*
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
protected function artisan(string $command, array $parameters = [], ?OutputStyle $output = null): int
|
||||
{
|
||||
return Artisan::call($command, $parameters, $output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Platform\Updates;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Identity\Permissions\Permission;
|
||||
use App\Modules\Identity\Permissions\PermissionChecker;
|
||||
use App\Modules\Identity\StaffAccounts;
|
||||
use App\Modules\Platform\Capabilities\Capability;
|
||||
use App\Modules\Platform\Capabilities\CapabilityRegistry;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
|
||||
/**
|
||||
* "An update just landed — has the administrator been told what was in
|
||||
* it?" One answer, shared by the middleware that redirects and the
|
||||
* controller that renders.
|
||||
*
|
||||
* Deliberately one person and one time. The update happened to the
|
||||
* *installation*, and interrupting five staff members with the same
|
||||
* congratulation — each of whom then has to dismiss a page they did not
|
||||
* ask for — would turn a nice moment into a nuisance that gets asked
|
||||
* about in a support thread. So it goes to whoever administers this
|
||||
* installation, once, and stays reachable at its own address for anybody
|
||||
* who wants it afterwards.
|
||||
*/
|
||||
class UpdateWelcome
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Settings $settings,
|
||||
private readonly CapabilityRegistry $capabilities,
|
||||
private readonly PermissionChecker $permissions,
|
||||
private readonly StaffAccounts $staff,
|
||||
private readonly ReleaseNotes $notes,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Whether this user should be taken to the welcome page right now.
|
||||
*/
|
||||
public function isWaitingFor(User $user): bool
|
||||
{
|
||||
return $this->pending() !== null
|
||||
&& $this->isTheMainAdministrator($user)
|
||||
&& $this->mayRead($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this user may open the page at all, update or no update.
|
||||
*
|
||||
* Carries the About screen's environment gate verbatim — view_system_info
|
||||
* plus SystemUpdates — because this answers the same question that one
|
||||
* does, in more detail. The capability is what keeps it off managed
|
||||
* installations, where nobody running this application performed an
|
||||
* update and "thank you for updating" would be addressed to the wrong
|
||||
* person entirely.
|
||||
*/
|
||||
public function mayRead(User $user): bool
|
||||
{
|
||||
return $user->isStaff()
|
||||
&& $this->capabilities->has(Capability::SystemUpdates)
|
||||
&& $this->permissions->allows($user, Permission::ViewSystemInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* The versions an unseen update moved between, or null if there is
|
||||
* nothing waiting.
|
||||
*
|
||||
* @return array{from: string, to: string}|null
|
||||
*/
|
||||
public function pending(): ?array
|
||||
{
|
||||
$to = $this->string(Setting::UpdateWelcomeTo);
|
||||
|
||||
if ($to === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ['from' => $this->string(Setting::UpdateWelcomeFrom), 'to' => $to];
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget the pending update, so the redirect happens exactly once.
|
||||
*
|
||||
* Only the marker is cleared — the page itself keeps working, and
|
||||
* keeps describing the same release, because "I closed it by accident"
|
||||
* should not be an unrecoverable mistake.
|
||||
*/
|
||||
public function dismiss(): void
|
||||
{
|
||||
$this->settings->set(Setting::UpdateWelcomeFrom, '');
|
||||
$this->settings->set(Setting::UpdateWelcomeTo, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* What to show: every release between the version that was running and
|
||||
* the one that is now.
|
||||
*
|
||||
* Falls back to the running version once the marker has been cleared,
|
||||
* so the page still has something to say when it is opened later from
|
||||
* a link rather than arrived at from an update.
|
||||
*
|
||||
* @return list<array{version: string, date: string, intro: list<string>, groups: list<array{heading: string, items: list<array{title: string, body: string}>}>}>
|
||||
*/
|
||||
public function releases(): array
|
||||
{
|
||||
return $this->notes->between($this->previousVersion(), $this->version());
|
||||
}
|
||||
|
||||
/**
|
||||
* The version this page is about — the one the update arrived at, or
|
||||
* the one running when it is opened cold.
|
||||
*/
|
||||
public function version(): string
|
||||
{
|
||||
$pending = $this->pending();
|
||||
|
||||
return $pending === null ? (string) config('projectsend.version') : $pending['to'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The version left behind, empty when unknown or when the page was
|
||||
* opened outside an update.
|
||||
*/
|
||||
public function previousVersion(): string
|
||||
{
|
||||
return $this->pending()['from'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this page is being read because an update just happened, as
|
||||
* opposed to opened from a link afterwards. The wording differs: one
|
||||
* says "you have updated", the other only "here is what this release
|
||||
* brought".
|
||||
*/
|
||||
public function isFresh(): bool
|
||||
{
|
||||
return $this->pending() !== null;
|
||||
}
|
||||
|
||||
private function isTheMainAdministrator(User $user): bool
|
||||
{
|
||||
return $this->staff->mainAdministrator()?->is($user) === true;
|
||||
}
|
||||
|
||||
private function string(Setting $setting): string
|
||||
{
|
||||
$value = $this->settings->get($setting);
|
||||
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Concerns;
|
||||
|
||||
use App\Support\VacatedSlug;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -16,9 +17,30 @@ use Illuminate\Support\Str;
|
||||
* create one without going through a form that requires it — so fall back to
|
||||
* deriving one from the name rather than failing at the database.
|
||||
*
|
||||
* Soft-deleted rows still count as collisions: their slugs remain reachable
|
||||
* until the row is really gone, and reusing one would resurrect the wrong
|
||||
* URL.
|
||||
* Deleting a row hands its slug back. A soft-deleted row is unreachable —
|
||||
* every public lookup goes through Eloquent, so the soft-delete scope has
|
||||
* already excluded it — and nothing in the application can restore one, so
|
||||
* a slug it kept holding would be reserved for a page that can never return.
|
||||
* Holding one is what burned the name of any deleted folder for good: the
|
||||
* unique index still saw the row, so the name could never be used again and
|
||||
* the screen could not say why, because the row it collided with is one the
|
||||
* interface will not show.
|
||||
*
|
||||
* The database is what forces the vacating to be a rewrite rather than a
|
||||
* softer lookup. Teaching the collision checks to ignore trashed rows is not
|
||||
* enough on its own — two rows would then both hold `report`, and the unique
|
||||
* index rejects that however the application feels about it. So the slug
|
||||
* moves into a namespace nothing else can occupy: `report__deleted-42`.
|
||||
* Underscores are the whole trick. Str::slug() turns them into hyphens and
|
||||
* Rules::slug() refuses them outright, so neither a derived slug nor a
|
||||
* hand-typed one can ever land on a vacated slug, whatever the row is called.
|
||||
*
|
||||
* The collision checks still count trashed rows anyway. It costs nothing, and
|
||||
* it keeps them honest about what the index will actually accept if a row is
|
||||
* ever soft-deleted by something that bypasses model events.
|
||||
*
|
||||
* Requires SoftDeletes: the collision check reaches for withTrashed(), and
|
||||
* vacating is only meaningful for a row that outlives its own delete.
|
||||
*
|
||||
* @phpstan-require-extends Model
|
||||
*/
|
||||
@@ -35,6 +57,10 @@ trait HasUniqueSlug
|
||||
$model->slug = static::uniqueSlugFrom($model->name);
|
||||
}
|
||||
});
|
||||
|
||||
static::deleted(function (self $model): void {
|
||||
static::vacateSlug($model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,6 +86,39 @@ trait HasUniqueSlug
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hands a soft-deleted row's slug back to the names still in use.
|
||||
*
|
||||
* Written through the query builder on purpose: this is bookkeeping, not
|
||||
* an edit somebody made, so it must not move updated_at or fire a second
|
||||
* round of model events on a row that is already on its way out.
|
||||
*/
|
||||
protected static function vacateSlug(self $model): void
|
||||
{
|
||||
// A row that is really gone took its slug with it. `deleted` fires
|
||||
// for forceDelete() too, and there is nothing left to rewrite.
|
||||
if ($model->isForceDeleting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blank($model->slug)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$vacated = VacatedSlug::for($model->slug, $model->getKey());
|
||||
|
||||
if ($vacated === $model->slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::query()->withTrashed()->whereKey($model->getKey())->toBase()->update(['slug' => $vacated]);
|
||||
|
||||
// Keep the in-memory row telling the truth for whatever the caller
|
||||
// does with it after the delete returns.
|
||||
$model->slug = $vacated;
|
||||
$model->syncOriginalAttribute('slug');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stands in when the name slugs to nothing at all — a name of only
|
||||
* punctuation or of characters Str::slug() drops entirely.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Support\Concerns\HasUniqueSlug;
|
||||
|
||||
/**
|
||||
* The slug a deleted row holds, so the name it used to have goes back into
|
||||
* circulation.
|
||||
*
|
||||
* Lives here rather than on HasUniqueSlug because the migration that vacates
|
||||
* slugs deleted before the behaviour existed needs the identical format, and
|
||||
* a trait constant cannot be reached through the trait's own name. Two copies
|
||||
* of this format that drift apart would leave rows the application no longer
|
||||
* recognises as vacated, so there is one.
|
||||
*
|
||||
* @see HasUniqueSlug
|
||||
*/
|
||||
final class VacatedSlug
|
||||
{
|
||||
/**
|
||||
* Underscores are the whole trick: Str::slug() turns them into hyphens and
|
||||
* Rules::slug() refuses them outright, so no derived slug and no
|
||||
* hand-typed one can ever collide with a vacated one.
|
||||
*/
|
||||
public const MARKER = '__deleted-';
|
||||
|
||||
/**
|
||||
* Column width. Slugs are ASCII by construction, but the truncation is
|
||||
* done in characters so a hand-written row cannot cut a byte in half.
|
||||
*/
|
||||
private const MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* @param int|string $key the row's own id, so two deleted rows that
|
||||
* shared a name do not collide with each other
|
||||
*/
|
||||
public static function for(string $slug, int|string $key): string
|
||||
{
|
||||
if (self::isVacated($slug)) {
|
||||
return $slug;
|
||||
}
|
||||
|
||||
$suffix = self::MARKER.$key;
|
||||
|
||||
return mb_substr($slug, 0, self::MAX_LENGTH - mb_strlen($suffix)).$suffix;
|
||||
}
|
||||
|
||||
public static function isVacated(string $slug): bool
|
||||
{
|
||||
return str_contains($slug, self::MARKER);
|
||||
}
|
||||
}
|
||||
+19
-13
@@ -17,7 +17,6 @@ use App\Modules\Platform\Http\Middleware\SetLocale;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
|
||||
@@ -33,17 +32,14 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
// Unset = trust nothing, which is correct for the shipped topology
|
||||
// (nginx talks to PHP-FPM directly and passes the real REMOTE_ADDR).
|
||||
// Behind anything else — a load balancer, Cloudflare, the hosted
|
||||
// Cloud ingress — this MUST name the proxy, or every client appears
|
||||
// to come from it: per-IP throttles collapse into one shared bucket
|
||||
// and the download IP log records the proxy instead of the client.
|
||||
$proxies = env('TRUSTED_PROXIES');
|
||||
|
||||
if (is_string($proxies) && $proxies !== '') {
|
||||
$middleware->trustProxies(at: $proxies === '*' ? '*' : explode(',', $proxies));
|
||||
}
|
||||
// Trusted proxies are configured in config/trustedproxy.php, NOT
|
||||
// here. This closure runs when the HTTP kernel is resolved, which is
|
||||
// before the dotenv bootstrapper has read .env, so env() returns null
|
||||
// here for anything that is not already a real environment variable —
|
||||
// silently, and only on web requests (artisan bootstraps in the other
|
||||
// order, so a CLI check reports the setting as working). The framework's
|
||||
// TrustProxies middleware is in the global stack either way and falls
|
||||
// back to that config key on its own.
|
||||
|
||||
$middleware->web(append: [
|
||||
// Binds every session to the password hash it was created under,
|
||||
@@ -56,7 +52,17 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
EnforceTwoFactor::class,
|
||||
SetLocale::class,
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
// Deliberately NOT here: AddLinkHeadersForPreloadedAssets. It
|
||||
// copies every Vite preload into a `Link:` response header,
|
||||
// and the head of the document already carries the identical
|
||||
// tags — twenty of them on the login page, more on a heavier
|
||||
// one. The copy is what a browser never reads and a proxy has
|
||||
// to buffer: it pushed /files past 6 KB of headers, where the
|
||||
// 4 KB proxy_buffer_size that nginx, and therefore Nginx Proxy
|
||||
// Manager, defaults to answers 502. Some pages fit and some do
|
||||
// not, so it reads as an intermittent fault rather than a
|
||||
// header that is always too big (#1664). Nothing is lost but
|
||||
// 103 Early Hints, which this application does not send.
|
||||
]);
|
||||
|
||||
// The API group gets none of the web stack above — no session, no
|
||||
|
||||
+109
-2
@@ -54,17 +54,112 @@ function projectsend_preflight_env(string $key): ?string
|
||||
* either null (configured — hand back to Laravel) or a [title, reason, fix]
|
||||
* triple describing what to do. No output, no exit — so it is testable.
|
||||
*
|
||||
* The order of the three questions is deliberate and not alphabetical:
|
||||
* missing dependencies come first because the fix the configuration branch
|
||||
* prints — `php artisan key:generate` — cannot itself run without the
|
||||
* autoloader, so reporting the key first would earn somebody a second and
|
||||
* more confusing error. Assets come last because a missing key is the more
|
||||
* fundamental problem of the two.
|
||||
*
|
||||
* @return array{0: string, 1: string, 2: string}|null
|
||||
*/
|
||||
function projectsend_preflight_failure(string $root): ?array
|
||||
{
|
||||
return projectsend_preflight_dependencies_failure($root)
|
||||
?? projectsend_preflight_configuration_failure($root)
|
||||
?? projectsend_preflight_assets_failure($root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Composer has never run here.
|
||||
*
|
||||
* Only reachable from source: a release zip ships vendored, which is the
|
||||
* whole reason INSTALL.md can promise you need neither Composer nor npm on
|
||||
* the server. On a `git clone`, public/index.php requires this file's
|
||||
* absence into a bare fatal on the very next line — no page, no log entry,
|
||||
* nothing to search for. Reported as exactly that by somebody following the
|
||||
* README's old quickstart (#1627).
|
||||
*
|
||||
* @return array{0: string, 1: string, 2: string}|null
|
||||
*/
|
||||
function projectsend_preflight_dependencies_failure(string $root): ?array
|
||||
{
|
||||
if (is_file($root.'/vendor/autoload.php')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'ProjectSend is not installed yet',
|
||||
'The PHP dependencies are missing — there is no <code>vendor/autoload.php</code>. A release '
|
||||
.'zip ships with them already in place; a <code>git clone</code> does not, and installs '
|
||||
.'them as its first step.',
|
||||
"Install the dependencies:
|
||||
|
||||
composer install
|
||||
|
||||
"
|
||||
.'On the development Docker stack: docker compose exec app composer install',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The frontend has never been built.
|
||||
*
|
||||
* `public/hot` counts as built: laravel-vite-plugin writes it while
|
||||
* `npm run dev` is running, and every asset is then served by that dev
|
||||
* server rather than out of public/build. Stopping a developer who has a dev
|
||||
* server running would make this guard worse than the exception it replaces.
|
||||
*
|
||||
* @return array{0: string, 1: string, 2: string}|null
|
||||
*/
|
||||
function projectsend_preflight_assets_failure(string $root): ?array
|
||||
{
|
||||
if (is_file($root.'/public/build/manifest.json') || is_file($root.'/public/hot')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'ProjectSend is not built yet',
|
||||
'The frontend has not been compiled — there is no '
|
||||
.'<code>public/build/manifest.json</code>. A release zip ships it already built; a '
|
||||
.'<code>git clone</code> does not. Without it every page fails while rendering, which '
|
||||
.'is a stack trace rather than an answer.',
|
||||
"Build the frontend:
|
||||
|
||||
npm ci
|
||||
npm run build",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this installation has been configured at all — the original and
|
||||
* most common first-run failure, kept exactly as it was.
|
||||
*
|
||||
* @return array{0: string, 1: string, 2: string}|null
|
||||
*/
|
||||
function projectsend_preflight_configuration_failure(string $root): ?array
|
||||
{
|
||||
$appKey = projectsend_preflight_env('APP_KEY');
|
||||
$envFile = $root.'/.env';
|
||||
$hasEnvFile = is_file($envFile);
|
||||
|
||||
// A .env that is there but unreadable looks exactly like one that was
|
||||
// never created — is_file() is false either way, since it has to follow
|
||||
// the link and stat the target — and the two need opposite advice.
|
||||
// Reported from the official Docker image, where .env is a symlink into
|
||||
// storage/: the image left /var/www/html world-writable and owned by a
|
||||
// uid that no longer existed, so the kernel's fs.protected_symlinks
|
||||
// refused to let the php-fpm worker follow it. Every request said "no
|
||||
// .env file was found" while `docker exec ... cat .env`, as root, printed
|
||||
// it back perfectly — which is the most misleading pair of facts this
|
||||
// guard could possibly hand somebody.
|
||||
$unreadableEnv = $hasEnvFile ? ! is_readable($envFile) : is_link($envFile);
|
||||
|
||||
// Fall back to a cheap read of just APP_KEY from the file — we are not
|
||||
// going to boot Dotenv or parse the whole thing for one value.
|
||||
if ($appKey === null && $hasEnvFile) {
|
||||
// going to boot Dotenv or parse the whole thing for one value. Skipped
|
||||
// when the file cannot be read, or file() emits a PHP warning into the
|
||||
// page this guard exists to keep clean.
|
||||
if ($appKey === null && $hasEnvFile && ! $unreadableEnv) {
|
||||
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
|
||||
if (preg_match('/^\s*(?:export\s+)?APP_KEY\s*=\s*(.*)$/', $line, $m) === 1) {
|
||||
// Strip the surrounding quotes Dotenv would also strip.
|
||||
@@ -79,6 +174,18 @@ function projectsend_preflight_failure(string $root): ?array
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($unreadableEnv) {
|
||||
return [
|
||||
'ProjectSend cannot read its configuration',
|
||||
'A <code>.env</code> is in place, but the user PHP runs as cannot read it — or, if it '
|
||||
.'is a symlink, cannot follow it. Note that <code>root</code> is exempt from both '
|
||||
.'checks, so reading the file over <code>docker exec</code> or <code>sudo</code> '
|
||||
.'proves nothing here.',
|
||||
"Compare who owns the file with who PHP runs as, and check the directory holding it:\n\n"
|
||||
."ls -ln .env\nstat -c '%n %U %a' . .env",
|
||||
];
|
||||
}
|
||||
|
||||
if (! $hasEnvFile) {
|
||||
return [
|
||||
'ProjectSend is not configured yet',
|
||||
|
||||
@@ -21,6 +21,12 @@ services:
|
||||
ADMIN_NAME: ${ADMIN_NAME:-}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
|
||||
# The whole default stack declares one, so it comes back after a reboot
|
||||
# or a Docker restart instead of half of it coming back — the confusing
|
||||
# state, where the queue runs and the site is down (#1658). The dev-only
|
||||
# profile services below deliberately do not: you bring those up for a
|
||||
# session, not for the life of the machine.
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -39,6 +45,7 @@ services:
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
- ./docker/web/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
@@ -74,6 +81,11 @@ services:
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
- ../packages:/var/www/packages
|
||||
# Same reason the worker has one, plus a second: on a fresh clone this
|
||||
# exits until `composer install` has run, and without a restart policy it
|
||||
# then stays exited — scheduled work silently never happens, on the one
|
||||
# setup where nobody would think to check.
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -90,6 +102,7 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root}
|
||||
volumes:
|
||||
- db-data:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Loopback only: this forward exists for host-side DB GUIs, not for
|
||||
# the network. Without the prefix Docker publishes on 0.0.0.0 and
|
||||
@@ -104,6 +117,7 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
|
||||
+2
-7
@@ -8,12 +8,6 @@
|
||||
"framework"
|
||||
],
|
||||
"license": "GPL-2.0-or-later",
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/projectsend/community-modules"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"bacon/bacon-qr-code": "^3.1",
|
||||
@@ -26,8 +20,9 @@
|
||||
"laravel/socialite": "^5.29",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"league/flysystem-aws-s3-v3": "^3.29",
|
||||
"league/flysystem-google-cloud-storage": "^3.34",
|
||||
"pragmarx/google2fa": "^9.0",
|
||||
"projectsend/community-modules": "*",
|
||||
"projectsend/community-modules": "^1.0",
|
||||
"stevebauman/purify": "^6.3",
|
||||
"tightenco/ziggy": "^2.4"
|
||||
},
|
||||
|
||||
Generated
+658
-14
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "cfcaf0c1e3208e2f96b6879bb0eaf3a7",
|
||||
"content-hash": "3d1b9f16a86d22643087b0b575807b6e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -1168,6 +1168,450 @@
|
||||
],
|
||||
"time": "2025-12-03T09:33:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/auth",
|
||||
"version": "v1.53.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/google-auth-library-php.git",
|
||||
"reference": "d677d0b0c4bd52ab222a85df8e74e0d491c0cc5a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/d677d0b0c4bd52ab222a85df8e74e0d491c0cc5a",
|
||||
"reference": "d677d0b0c4bd52ab222a85df8e74e0d491c0cc5a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"firebase/php-jwt": "^6.0||^7.0",
|
||||
"guzzlehttp/guzzle": "^7.8.2||^8.0",
|
||||
"guzzlehttp/psr7": "^2.6.3||^3.0",
|
||||
"php": "^8.1",
|
||||
"psr/cache": "^2.0||^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-message": "^1.1||^2.0",
|
||||
"psr/log": "^2.0||^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/promises": "^2.0.3||^3.0",
|
||||
"kelvinmo/simplejwt": "^1.1.0",
|
||||
"phpseclib/phpseclib": "^3.0.35",
|
||||
"phpspec/prophecy-phpunit": "^2.1",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"sebastian/comparator": ">=1.2.3",
|
||||
"squizlabs/php_codesniffer": "^4.0",
|
||||
"symfony/filesystem": "^6.3||^7.3",
|
||||
"symfony/process": "^6.0||^7.0",
|
||||
"webmozart/assert": "^1.11||^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\Auth\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "Google Auth Library for PHP",
|
||||
"homepage": "https://github.com/google/google-auth-library-php",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"google",
|
||||
"oauth2"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://cloud.google.com/php/docs/reference/auth/latest",
|
||||
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
|
||||
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.53.0"
|
||||
},
|
||||
"time": "2026-07-22T22:36:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/cloud-core",
|
||||
"version": "v1.73.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/google-cloud-php-core.git",
|
||||
"reference": "883bc97bdcd5e09552eb82cb47a752271a310c7a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/google-cloud-php-core/zipball/883bc97bdcd5e09552eb82cb47a752271a310c7a",
|
||||
"reference": "883bc97bdcd5e09552eb82cb47a752271a310c7a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"google/auth": "^1.53",
|
||||
"google/gax": "^1.38.0",
|
||||
"guzzlehttp/guzzle": "^7.8.2||^8.0",
|
||||
"guzzlehttp/promises": "^2.0.3||^3.0",
|
||||
"guzzlehttp/psr7": "^2.6.3||^3.0",
|
||||
"monolog/monolog": "^2.9||^3.0",
|
||||
"php": "^8.1",
|
||||
"psr/http-message": "^1.0||^2.0",
|
||||
"rize/uri-template": "~0.3||~0.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"erusev/parsedown": "^1.6",
|
||||
"google/cloud-common-protos": "~0.5||^1.0",
|
||||
"nikic/php-parser": "^5.6",
|
||||
"opis/closure": "^3.7|^4.0",
|
||||
"phpdocumentor/reflection": "^6.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.3.3||^6.0",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"squizlabs/php_codesniffer": "3.*"
|
||||
},
|
||||
"suggest": {
|
||||
"opis/closure": "May be used to serialize closures to process jobs in the batch daemon. Please require version ^3.",
|
||||
"symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: 3.3.x-dev#1ba6ac9"
|
||||
},
|
||||
"bin": [
|
||||
"bin/google-cloud-batch"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"component": {
|
||||
"id": "cloud-core",
|
||||
"path": "Core",
|
||||
"entry": "src/ServiceBuilder.php",
|
||||
"target": "googleapis/google-cloud-php-core.git"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\Cloud\\Core\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "Google Cloud PHP shared dependency, providing functionality useful to all components.",
|
||||
"support": {
|
||||
"source": "https://github.com/googleapis/google-cloud-php-core/tree/v1.73.2"
|
||||
},
|
||||
"time": "2026-08-14T23:32:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/cloud-storage",
|
||||
"version": "v2.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/google-cloud-php-storage.git",
|
||||
"reference": "1e90d4a1bebd8cef366addce9e57bc4d9ac9c760"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/google-cloud-php-storage/zipball/1e90d4a1bebd8cef366addce9e57bc4d9ac9c760",
|
||||
"reference": "1e90d4a1bebd8cef366addce9e57bc4d9ac9c760",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"google/cloud-core": "^1.72.0",
|
||||
"php": "^8.1",
|
||||
"ramsey/uuid": "^4.2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"erusev/parsedown": "^1.6",
|
||||
"google/cloud-pubsub": "^2.0",
|
||||
"nikic/php-parser": "^5",
|
||||
"phpdocumentor/reflection": "^6.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.3.3",
|
||||
"phpseclib/phpseclib": "^2.0||^3.0",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"squizlabs/php_codesniffer": "3.*"
|
||||
},
|
||||
"suggest": {
|
||||
"google/cloud-pubsub": "May be used to register a topic to receive bucket notifications.",
|
||||
"phpseclib/phpseclib": "May be used in place of OpenSSL for creating signed Cloud Storage URLs. Please require version ^2."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"component": {
|
||||
"id": "cloud-storage",
|
||||
"path": "Storage",
|
||||
"entry": "src/StorageClient.php",
|
||||
"target": "googleapis/google-cloud-php-storage.git"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\Cloud\\Storage\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "Cloud Storage Client for PHP",
|
||||
"support": {
|
||||
"source": "https://github.com/googleapis/google-cloud-php-storage/tree/v2.5.2"
|
||||
},
|
||||
"time": "2026-08-14T23:32:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/common-protos",
|
||||
"version": "4.14.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/common-protos-php.git",
|
||||
"reference": "4eb6813b8068653e055fc8a63dbda3446f3e8869"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/common-protos-php/zipball/4eb6813b8068653e055fc8a63dbda3446f3e8869",
|
||||
"reference": "4eb6813b8068653e055fc8a63dbda3446f3e8869",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"google/protobuf": "^4.31||^5.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"component": {
|
||||
"id": "common-protos",
|
||||
"path": "CommonProtos",
|
||||
"entry": "README.md",
|
||||
"target": "googleapis/common-protos-php.git"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\Api\\": "src/Api",
|
||||
"Google\\Iam\\": "src/Iam",
|
||||
"Google\\Rpc\\": "src/Rpc",
|
||||
"Google\\Type\\": "src/Type",
|
||||
"Google\\Cloud\\": "src/Cloud",
|
||||
"GPBMetadata\\Google\\Api\\": "metadata/Api",
|
||||
"GPBMetadata\\Google\\Iam\\": "metadata/Iam",
|
||||
"GPBMetadata\\Google\\Rpc\\": "metadata/Rpc",
|
||||
"GPBMetadata\\Google\\Type\\": "metadata/Type",
|
||||
"GPBMetadata\\Google\\Cloud\\": "metadata/Cloud",
|
||||
"GPBMetadata\\Google\\Logging\\": "metadata/Logging"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "Google API Common Protos for PHP",
|
||||
"homepage": "https://github.com/googleapis/common-protos-php",
|
||||
"keywords": [
|
||||
"google"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/googleapis/common-protos-php/tree/v4.14.1"
|
||||
},
|
||||
"time": "2026-06-17T23:07:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/gax",
|
||||
"version": "v1.48.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/gax-php.git",
|
||||
"reference": "637096f2c70f6bc903ba24aee3a0d974d739aba8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/gax-php/zipball/637096f2c70f6bc903ba24aee3a0d974d739aba8",
|
||||
"reference": "637096f2c70f6bc903ba24aee3a0d974d739aba8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"google/auth": "^1.53",
|
||||
"google/common-protos": "^4.9",
|
||||
"google/grpc-gcp": "^0.4",
|
||||
"google/longrunning": "~0.4",
|
||||
"google/protobuf": "^4.31||^5.34",
|
||||
"grpc/grpc": "^1.13",
|
||||
"guzzlehttp/promises": "^2.0.3||^3.0",
|
||||
"guzzlehttp/psr7": "^2.6.3||^3.0",
|
||||
"php": "^8.1",
|
||||
"ramsey/uuid": "^4.0"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-protobuf": "<4.31.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"google/cloud-tools": "^0.16.1",
|
||||
"phpspec/prophecy-phpunit": "^2.1",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"component": {
|
||||
"id": "gax",
|
||||
"path": "Gax",
|
||||
"entry": "README.md",
|
||||
"target": "googleapis/gax-php.git"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\ApiCore\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "Google API Core for PHP",
|
||||
"homepage": "https://github.com/googleapis/gax-php",
|
||||
"keywords": [
|
||||
"google"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/googleapis/gax-php/issues",
|
||||
"source": "https://github.com/googleapis/gax-php/tree/v1.48.0"
|
||||
},
|
||||
"time": "2026-08-14T23:32:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/grpc-gcp",
|
||||
"version": "0.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/GoogleCloudPlatform/grpc-gcp-php.git",
|
||||
"reference": "1049c0c15b6a1789fdeb52af688a94d540932469"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/GoogleCloudPlatform/grpc-gcp-php/zipball/1049c0c15b6a1789fdeb52af688a94d540932469",
|
||||
"reference": "1049c0c15b6a1789fdeb52af688a94d540932469",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"google/auth": "^1.3",
|
||||
"google/protobuf": "^v3.25.3||^4.26.1||^5.0",
|
||||
"grpc/grpc": "^v1.13.0",
|
||||
"php": "^8.0",
|
||||
"psr/cache": "^1.0.1||^2.0.0||^3.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"google/cloud-spanner": "^1.7",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Grpc\\Gcp\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"src/generated/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "gRPC GCP library for channel management",
|
||||
"support": {
|
||||
"issues": "https://github.com/GoogleCloudPlatform/grpc-gcp-php/issues",
|
||||
"source": "https://github.com/GoogleCloudPlatform/grpc-gcp-php/tree/v0.4.2"
|
||||
},
|
||||
"time": "2026-03-12T22:56:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/longrunning",
|
||||
"version": "0.8.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/php-longrunning.git",
|
||||
"reference": "309705016290679e6fe14b727f4b1a3e04ae52f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/php-longrunning/zipball/309705016290679e6fe14b727f4b1a3e04ae52f4",
|
||||
"reference": "309705016290679e6fe14b727f4b1a3e04ae52f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"google/gax": "^1.38.0",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"component": {
|
||||
"id": "longrunning",
|
||||
"path": "LongRunning",
|
||||
"entry": null,
|
||||
"target": "googleapis/php-longrunning"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\LongRunning\\": "src/LongRunning",
|
||||
"Google\\ApiCore\\LongRunning\\": "src/ApiCore/LongRunning",
|
||||
"GPBMetadata\\Google\\Longrunning\\": "metadata/Longrunning"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "Google LongRunning Client for PHP",
|
||||
"support": {
|
||||
"source": "https://github.com/googleapis/php-longrunning/tree/v0.8.1"
|
||||
},
|
||||
"time": "2026-08-14T23:32:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/protobuf",
|
||||
"version": "v5.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/protocolbuffers/protobuf-php.git",
|
||||
"reference": "9c105104b54709ecd902494ab340ed2122789b2d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/9c105104b54709ecd902494ab340ed2122789b2d",
|
||||
"reference": "9c105104b54709ecd902494ab340ed2122789b2d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": ">=11.5.50 <12.0.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "Need to support JSON deserialization"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\Protobuf\\": "src/Google/Protobuf",
|
||||
"GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "proto library for PHP",
|
||||
"homepage": "https://developers.google.com/protocol-buffers/",
|
||||
"keywords": [
|
||||
"proto"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v5.36.0"
|
||||
},
|
||||
"time": "2026-08-20T13:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"version": "v1.1.4",
|
||||
@@ -1230,6 +1674,50 @@
|
||||
],
|
||||
"time": "2025-12-27T19:43:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "grpc/grpc",
|
||||
"version": "1.82.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/grpc/grpc-php.git",
|
||||
"reference": "be984cb608f21e96453b3cfe54c748cc7b192250"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/grpc/grpc-php/zipball/be984cb608f21e96453b3cfe54c748cc7b192250",
|
||||
"reference": "be984cb608f21e96453b3cfe54c748cc7b192250",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"google/auth": "^v1.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-protobuf": "For better performance, install the protobuf C extension.",
|
||||
"google/protobuf": "To get started using grpc quickly, install the native protobuf library."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Grpc\\": "src/lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "gRPC library for PHP",
|
||||
"homepage": "https://grpc.io",
|
||||
"keywords": [
|
||||
"rpc"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/grpc/grpc-php/tree/v1.82.0"
|
||||
},
|
||||
"time": "2026-07-03T09:39:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.15.2",
|
||||
@@ -2648,6 +3136,54 @@
|
||||
},
|
||||
"time": "2026-07-01T23:25:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-google-cloud-storage",
|
||||
"version": "3.34.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem-google-cloud-storage.git",
|
||||
"reference": "7ae8cd9ec58dd4b387ee1f7349e728ed8c455b09"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem-google-cloud-storage/zipball/7ae8cd9ec58dd4b387ee1f7349e728ed8c455b09",
|
||||
"reference": "7ae8cd9ec58dd4b387ee1f7349e728ed8c455b09",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"google/cloud-storage": "^1.23 || ^2.0",
|
||||
"league/flysystem": "^3.10.0",
|
||||
"league/mime-type-detection": "^1.0.0",
|
||||
"php": "^8.0.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\GoogleCloudStorage\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frankdejonge.nl"
|
||||
}
|
||||
],
|
||||
"description": "Google Cloud Storage adapter for Flysystem.",
|
||||
"keywords": [
|
||||
"Flysystem",
|
||||
"filesystem",
|
||||
"gcs",
|
||||
"google cloud storage"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/thephpleague/flysystem-google-cloud-storage/tree/3.34.0"
|
||||
},
|
||||
"time": "2026-05-12T08:30:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-local",
|
||||
"version": "3.31.0",
|
||||
@@ -4052,16 +4588,16 @@
|
||||
},
|
||||
{
|
||||
"name": "projectsend/community-modules",
|
||||
"version": "dev-main",
|
||||
"version": "v1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/projectsend/community-modules.git",
|
||||
"reference": "6f37aebd6b12f5a3411a33ea0823991529641955"
|
||||
"reference": "311883a7bdca91671ab84891443400e8c397e1df"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/projectsend/community-modules/zipball/6f37aebd6b12f5a3411a33ea0823991529641955",
|
||||
"reference": "6f37aebd6b12f5a3411a33ea0823991529641955",
|
||||
"url": "https://api.github.com/repos/projectsend/community-modules/zipball/311883a7bdca91671ab84891443400e8c397e1df",
|
||||
"reference": "311883a7bdca91671ab84891443400e8c397e1df",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4074,7 +4610,6 @@
|
||||
"orchestra/testbench": "^10.0",
|
||||
"pestphp/pest": "^3.8"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
@@ -4088,20 +4623,65 @@
|
||||
"ProjectSend\\CommunityModules\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"ProjectSend\\CommunityModules\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"GPL-2.0-or-later"
|
||||
],
|
||||
"description": "Community-exclusive feature modules for ProjectSend — free software, never part of the hosted Cloud edition.",
|
||||
"support": {
|
||||
"source": "https://github.com/projectsend/community-modules/tree/main",
|
||||
"issues": "https://github.com/projectsend/community-modules/issues"
|
||||
"issues": "https://github.com/projectsend/community-modules/issues",
|
||||
"source": "https://github.com/projectsend/community-modules/tree/v1.0.0"
|
||||
},
|
||||
"time": "2026-08-13T13:55:16+00:00"
|
||||
"time": "2026-08-16T18:45:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/cache.git",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Cache\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for caching libraries",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"psr",
|
||||
"psr-6"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/cache/tree/3.0.0"
|
||||
},
|
||||
"time": "2021-02-03T23:26:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
@@ -4792,6 +5372,70 @@
|
||||
},
|
||||
"time": "2026-06-18T03:57:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "rize/uri-template",
|
||||
"version": "0.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rize/UriTemplate.git",
|
||||
"reference": "7ad22944daede547b4542e1c977ec4a81aa20832"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rize/UriTemplate/zipball/7ad22944daede547b4542e1c977ec4a81aa20832",
|
||||
"reference": "7ad22944daede547b4542e1c977ec4a81aa20832",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.63",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpunit/phpunit": "~10.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Rize\\": "src/Rize"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marut K",
|
||||
"homepage": "http://twitter.com/rezigned"
|
||||
}
|
||||
],
|
||||
"description": "PHP URI Template (RFC 6570) supports both expansion & extraction",
|
||||
"keywords": [
|
||||
"RFC 6570",
|
||||
"template",
|
||||
"uri"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rize/UriTemplate/issues",
|
||||
"source": "https://github.com/rize/UriTemplate/tree/0.4.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.paypal.me/rezigned",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/rezigned",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/rize-uri-template",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-07T15:30:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
"version": "1.93.1",
|
||||
|
||||
+37
-1
@@ -37,12 +37,48 @@ return [
|
||||
'root' => storage_path('app/files'),
|
||||
'serve' => false,
|
||||
'throw' => false,
|
||||
|
||||
// A download is not served by PHP. PHP authorizes it and
|
||||
// hands the web server the path with X-Accel-Redirect, so the
|
||||
// web server has to open a file PHP wrote. Where the two are
|
||||
// different users — cPanel and Plesk commonly do this — it
|
||||
// cannot: uploads land 0600 inside a 0700 directory, and
|
||||
// traversing 0700 means *being* its owner. Everything else on
|
||||
// the site keeps working, which is what makes it hard to
|
||||
// place (#1668).
|
||||
//
|
||||
// FILES_WEB_SERVER_READABLE relaxes both to 0644/0755. Opt-in,
|
||||
// because it is strictly weaker: those modes are readable by
|
||||
// every account on the machine, and on a single-user host they
|
||||
// buy nothing. The files stay off the web either way — nginx
|
||||
// only reaches them through an `internal` location.
|
||||
//
|
||||
// The two halves are enforced differently, and only one of
|
||||
// them is absolute. `visibility` makes Flysystem chmod each
|
||||
// file after writing it, so 0644 holds whatever the umask is.
|
||||
// Directories get no chmod — they are created by mkdir(), and
|
||||
// mkdir() masks its mode argument with the process umask — so
|
||||
// 0755 here is a ceiling, not a guarantee. A pool running at
|
||||
// umask 0077 still produces 0700 and still cannot be
|
||||
// traversed; INSTALL.md covers fixing that, because it cannot
|
||||
// be fixed from this file.
|
||||
// Spread rather than two ternaries so that leaving the flag
|
||||
// off is not merely equivalent to the old configuration but
|
||||
// literally it — no install that does not need this sees its
|
||||
// file modes change.
|
||||
...(env('FILES_WEB_SERVER_READABLE', false)
|
||||
? ['visibility' => 'public', 'permissions' => ['dir' => ['private' => 0755]]]
|
||||
: []),
|
||||
],
|
||||
|
||||
// Laravel's stock private disk. Nothing in this application writes
|
||||
// to it, and `serve` is off because the framework's /storage route
|
||||
// would otherwise hand out whatever ended up there — a door with
|
||||
// nothing behind it today is still a door.
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'serve' => false,
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
|
||||
+12
-1
@@ -72,7 +72,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => '2.0.0',
|
||||
'version' => '2.1.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -80,13 +80,24 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Read through App\Modules\Platform\OfficialLinks rather than
|
||||
// directly: which of the two front doors "website" means, and whether
|
||||
// the donation link is offered at all, both depend on the edition.
|
||||
'links' => [
|
||||
'website' => 'https://www.projectsend.org/',
|
||||
// The hosted service's own front door. A managed installation
|
||||
// links here instead — including from the "Powered by" line on
|
||||
// client-facing pages and outgoing email.
|
||||
'website_cloud' => 'https://www.projectsend.cloud/',
|
||||
// Where this code lives, and the same repository
|
||||
// CheckForUpdatesCommand asks for the latest release. v1 remains
|
||||
// available at github.com/projectsend/legacy.
|
||||
'source' => 'https://github.com/projectsend/projectsend',
|
||||
'open_collective' => 'https://opencollective.com/projectsend',
|
||||
// Kept identical to the invitation update.sh prints when an update
|
||||
// finishes — the two are the same offer, made in the terminal and
|
||||
// then again on the screen the administrator lands on.
|
||||
'discord' => 'https://discord.gg/VT9n6cyvXT',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
// Read here rather than in bootstrap/app.php's withMiddleware closure: that
|
||||
// closure runs when the HTTP kernel is resolved, which under PHP-FPM is
|
||||
// BEFORE the dotenv bootstrapper loads .env, so env('TRUSTED_PROXIES') is
|
||||
// null there on every web request (it works in artisan, which bootstraps
|
||||
// first — the discrepancy is invisible in CLI testing). Config files load
|
||||
// after dotenv, and the framework's TrustProxies middleware falls back to
|
||||
// this key on its own.
|
||||
return [
|
||||
'proxies' => env('TRUSTED_PROXIES'),
|
||||
];
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Support\VacatedSlug;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Hands back the slugs held by rows deleted before slugs were vacated on
|
||||
* delete.
|
||||
*
|
||||
* Without this the fix only helps installations that have never deleted
|
||||
* anything: every folder, file and group already in the trash goes on
|
||||
* occupying its name, which is exactly the state being reported.
|
||||
*
|
||||
* Rewriting a deleted row's slug changes nothing anybody can see. A trashed
|
||||
* row is excluded from every public lookup by the soft-delete scope, so its
|
||||
* slug addresses no page — it was only ever visible as the reason a new row
|
||||
* could not have the name.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
foreach (['files', 'folders', 'groups'] as $table) {
|
||||
DB::table($table)
|
||||
->whereNotNull('deleted_at')
|
||||
->whereNotNull('slug')
|
||||
->where('slug', 'not like', '%'.VacatedSlug::MARKER.'%')
|
||||
// Chunked because an installation that has been running a
|
||||
// while can have a lot of these, and each row's new slug
|
||||
// depends on its own id rather than on anything set-wide.
|
||||
->orderBy('id')
|
||||
->chunkById(500, function (Collection $rows) use ($table): void {
|
||||
foreach ($rows as $row) {
|
||||
DB::table($table)
|
||||
->where('id', $row->id)
|
||||
->update(['slug' => VacatedSlug::for((string) $row->slug, (int) $row->id)]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliberately does not put the old slugs back. By the time this is rolled
|
||||
* back the names may well have been taken by live rows, and restoring them
|
||||
* would break the unique index this migration exists to stop tripping over.
|
||||
* A deleted row keeping a vacated slug harms nothing on older code — it
|
||||
* reads as an unusual name on a row nobody can see.
|
||||
*/
|
||||
public function down(): void {}
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('external_storage_settings', function (Blueprint $table) {
|
||||
// Every row that exists predates the choice, and every one of
|
||||
// them is S3 — so the default is what keeps this migration
|
||||
// invisible to anyone already using external storage.
|
||||
$table->string('provider')->default('s3')->after('active');
|
||||
|
||||
// A service account key is a ~2 KB JSON document, not a
|
||||
// password, so it gets its own encrypted column rather than
|
||||
// sharing `secret` with S3. The two are validated
|
||||
// differently, labelled differently and shown differently,
|
||||
// and one column meaning two things is how that gets
|
||||
// confusing later.
|
||||
$table->text('key_file')->nullable()->after('secret');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('external_storage_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['provider', 'key_file']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -20,12 +20,30 @@ mkdir -p storage/app/files storage/app/private storage/app/public storage/app/up
|
||||
chown www-data:www-data storage storage/app storage/app/files storage/app/private storage/app/public
|
||||
chown -R www-data:www-data storage/app/uploads-tmp storage/framework storage/logs bootstrap/cache
|
||||
|
||||
# The worker and the scheduler exec straight into artisan, which cannot run
|
||||
# without the autoloader — so on a fresh clone, before `composer install`,
|
||||
# both died instantly and `restart: unless-stopped` brought them back to die
|
||||
# again, once a second, forever. The log that was filling up is the same one
|
||||
# somebody needs to read to find out what to do (#1627).
|
||||
#
|
||||
# Exit rather than wait: this container has nothing useful to do until the
|
||||
# dependencies exist, and compose will keep retrying — but slowly enough to
|
||||
# leave the log readable, and it recovers on its own the moment they appear.
|
||||
if [ "$1" != "php-fpm" ] && [ ! -f vendor/autoload.php ]; then
|
||||
echo "projectsend: the PHP dependencies are not installed — there is no vendor/autoload.php." >&2
|
||||
echo "projectsend: run 'docker compose exec app composer install' (see CONTRIBUTING.md)." >&2
|
||||
sleep 15
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First-boot bootstrap runs only for the FPM service (the worker execs
|
||||
# straight through) and only when the app is actually installed.
|
||||
if [ "$1" = "php-fpm" ] && [ -f vendor/autoload.php ]; then
|
||||
su-exec www-data php artisan migrate --force
|
||||
su-exec www-data php artisan storage:link --force
|
||||
su-exec www-data php artisan projectsend:ensure-roles
|
||||
# The same command the official image and update.sh run — migrate,
|
||||
# roles, storage link, compiled caches, workers. See
|
||||
# UpdateInstallation; there is deliberately only one definition of it.
|
||||
su-exec www-data php artisan projectsend:update
|
||||
|
||||
# Unattended provisioning: create the first administrator from the
|
||||
# environment. Without these, the web setup screen prompts instead.
|
||||
|
||||
@@ -66,6 +66,38 @@ RUN delgroup www-data 2>/dev/null || true \
|
||||
&& addgroup -g 1000 www-data \
|
||||
&& adduser -D -H -u 1000 -G www-data www-data
|
||||
|
||||
# nginx must run as that same user, and the alpine package does not: it
|
||||
# ships `user nginx;` (uid 100) in its own nginx.conf. The two identities
|
||||
# have to match because php-fpm hands nginx files to serve — protected
|
||||
# downloads and thumbnails go out by X-Accel-Redirect — and the
|
||||
# application creates directories on demand at Flysystem's private mode,
|
||||
# 0700. Traversing one of those means *being* its owner; there are no
|
||||
# group or other bits to fall back on, so a differently-owned nginx worker
|
||||
# answers 403 on the first thumbnail it is asked for.
|
||||
#
|
||||
# Changing the directive is only half of it. /var/lib/nginx and its tmp/
|
||||
# come from the package owned by the old user, and a worker cannot reach
|
||||
# the temp directories underneath a parent it may not traverse — nginx
|
||||
# recreates the leaves at boot as the new user, so they look right while
|
||||
# every request nginx buffers to disk (every upload chunk, which is what
|
||||
# client_max_body_size is set high for) fails with a bare 500 that never
|
||||
# reaches php-fpm.
|
||||
RUN sed -i 's/^user nginx;/user www-data;/' /etc/nginx/nginx.conf \
|
||||
&& chown -R www-data:www-data /var/lib/nginx
|
||||
|
||||
# nginx is the one process here that does not log where supervisord can
|
||||
# see it. supervisord captures what a program writes to its own stdout,
|
||||
# but nginx opens the files named in the package's nginx.conf the moment
|
||||
# it reads its config, so its access and error logs went to
|
||||
# /var/log/nginx/ inside the container — invisible to `docker logs`, which
|
||||
# is the only place anybody looks. That is where the reason for every 502
|
||||
# and every 403 was being written, so a reverse-proxy problem presented as
|
||||
# no logs at all on either side. Point both at the container's own
|
||||
# streams, which is what the rest of this image already does.
|
||||
RUN sed -i -e 's#^error_log .*#error_log /dev/stderr warn;#' \
|
||||
-e 's#^\(\s*\)access_log .*#\1access_log /dev/stdout main;#' \
|
||||
/etc/nginx/nginx.conf
|
||||
|
||||
COPY docker/production/php.ini /usr/local/etc/php/conf.d/projectsend.ini
|
||||
COPY docker/production/www-pool.conf /usr/local/etc/php-fpm.d/zz-www-pool.conf
|
||||
COPY docker/production/nginx.conf /etc/nginx/http.d/default.conf
|
||||
@@ -80,10 +112,24 @@ WORKDIR /var/www/html
|
||||
COPY --chown=www-data:www-data app/ /var/www/html/
|
||||
|
||||
# storage/ and bootstrap/cache must be writable by the runtime user.
|
||||
#
|
||||
# /var/www/html itself needs re-owning as well, and it is easy to miss: COPY
|
||||
# --chown re-owns what it copies *into* the directory, never the directory,
|
||||
# so it keeps what the base image gave it — uid 82 (the www-data this image
|
||||
# replaced above) and mode 1777, which php:*-fpm sets so that an image can
|
||||
# run as an arbitrary user. Left that way, the directory is world-writable,
|
||||
# sticky, and owned by nobody the container knows about, and the kernel's
|
||||
# fs.protected_symlinks (on by default on Ubuntu, Debian and most current
|
||||
# distributions) then refuses to let the php-fpm worker follow the .env
|
||||
# symlink the entrypoint puts there. Root is exempt, so `docker exec ... cat
|
||||
# .env` reads it back perfectly while every real request 503s with
|
||||
# "ProjectSend is not configured yet".
|
||||
RUN mkdir -p storage/app/files storage/framework/cache storage/framework/sessions \
|
||||
storage/framework/views storage/logs bootstrap/cache \
|
||||
&& chown -R www-data:www-data storage bootstrap/cache \
|
||||
&& chmod -R u+rwX storage bootstrap/cache \
|
||||
&& chown www-data:www-data /var/www/html \
|
||||
&& chmod 755 /var/www/html \
|
||||
&& mkdir -p /run/nginx
|
||||
|
||||
# The whole of storage/, not just storage/app/files. Uploaded files are the
|
||||
@@ -93,6 +139,14 @@ RUN mkdir -p storage/app/files storage/framework/cache storage/framework/session
|
||||
# session and makes every encrypted column unreadable.
|
||||
VOLUME ["/var/www/html/storage"]
|
||||
|
||||
# How the application knows it is this image and not a container somebody
|
||||
# built from a checkout — the two upgrade completely differently, and it is
|
||||
# the application that prints the instructions. See Installation. It has to
|
||||
# be set here rather than inferred at runtime: an operator who bind-mounts
|
||||
# over /var/www/html can hide any file the image ships as its evidence, and
|
||||
# an environment variable survives that.
|
||||
ENV PROJECTSEND_IMAGE=1
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# Laravel's health route (bootstrap/app.php: health: '/up'). Hitting it
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
#
|
||||
# 1. Edit the passwords and APP_URL below.
|
||||
# 2. docker compose -f compose.example.yaml up -d
|
||||
# 3. Open APP_URL. If you set ADMIN_EMAIL/ADMIN_PASSWORD the first
|
||||
# administrator already exists; otherwise the setup screen prompts.
|
||||
# 3. Open APP_URL — the setup screen creates your administrator account.
|
||||
#
|
||||
# This is the file the Docker Hub description points at, so it is written
|
||||
# for someone who has never seen the project before.
|
||||
@@ -58,11 +57,13 @@ services:
|
||||
# download log records the proxy's address.
|
||||
TRUSTED_PROXIES: "*"
|
||||
|
||||
# Optional: creates the first administrator so you skip the setup
|
||||
# screen. Ignored once any user exists.
|
||||
ADMIN_NAME: Administrator
|
||||
ADMIN_EMAIL: admin@example.com
|
||||
ADMIN_PASSWORD: change-me-admin
|
||||
# Optional: uncomment these — with a password of your own — to create
|
||||
# the first administrator unattended and skip the setup screen. Left
|
||||
# commented, the setup screen creates it instead. Ignored once any
|
||||
# user exists.
|
||||
# ADMIN_NAME: Administrator
|
||||
# ADMIN_EMAIL: admin@example.com
|
||||
# ADMIN_PASSWORD: change-me-admin
|
||||
volumes:
|
||||
# Every uploaded file lives here, along with the generated APP_KEY.
|
||||
# This is the volume to back up; losing it loses the data.
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
[](https://github.com/projectsend/projectsend)
|
||||
|
||||
[](https://github.com/projectsend/projectsend/releases "what is in each release, and the downloadable zip")
|
||||
[](https://hub.docker.com/r/projectsend/projectsend/tags "every published tag")
|
||||
[](https://hub.docker.com/r/projectsend/projectsend/tags "compressed size of the latest tag")
|
||||
[](https://github.com/projectsend/projectsend "source, issues and full documentation")
|
||||
[](https://discord.gg/VT9n6cyvXT "release news, and help when something is not behaving")
|
||||
[](https://github.com/projectsend/projectsend/blob/main/LICENSE "free software, and commercial licences for those who need them")
|
||||
|
||||
# ProjectSend
|
||||
|
||||
**Share files with your clients, from your own server.**
|
||||
|
||||
ProjectSend is a self-hosted application for getting files to the people you work with. You upload
|
||||
what you want to send, choose exactly who can see it, and each client signs in to their own private
|
||||
page to download it.
|
||||
|
||||
No public link passed around by email, no third-party service holding your clients' documents, no
|
||||
per-seat pricing. It runs on your server, and the files stay there.
|
||||
|
||||
This is the official image for the Community edition — free software under the GPL v2 or later.
|
||||
|
||||

|
||||
|
||||
*The dashboard — what is in the installation, and what has been happening in it. More screenshots
|
||||
are in the [README](https://github.com/projectsend/projectsend#screenshots).*
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
Save this as `compose.yaml`, change the passwords and `APP_URL`, then `docker compose up -d`.
|
||||
|
||||
```yaml
|
||||
name: projectsend
|
||||
|
||||
services:
|
||||
app:
|
||||
image: projectsend/projectsend:2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Put a TLS-terminating proxy in front of this in any real install.
|
||||
- "8080:80"
|
||||
environment:
|
||||
APP_URL: https://files.example.com
|
||||
APP_ENV: production
|
||||
APP_DEBUG: "false"
|
||||
|
||||
DB_HOST: db
|
||||
DB_DATABASE: projectsend
|
||||
DB_USERNAME: projectsend
|
||||
DB_PASSWORD: change-me-database
|
||||
|
||||
REDIS_HOST: redis
|
||||
CACHE_STORE: redis
|
||||
SESSION_DRIVER: redis
|
||||
QUEUE_CONNECTION: redis
|
||||
|
||||
# Required whenever anything sits between your visitors and this
|
||||
# container — including the reverse proxy you should be running.
|
||||
TRUSTED_PROXIES: "*"
|
||||
|
||||
# Optional: uncomment these — with a password of your own — to create
|
||||
# the first administrator unattended and skip the setup screen. Left
|
||||
# commented, the setup screen creates it instead. Ignored once any
|
||||
# user exists.
|
||||
# ADMIN_NAME: Administrator
|
||||
# ADMIN_EMAIL: admin@example.com
|
||||
# ADMIN_PASSWORD: change-me-admin
|
||||
volumes:
|
||||
# Every uploaded file lives here, along with the generated APP_KEY.
|
||||
- storage:/var/www/html/storage
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_DATABASE: projectsend
|
||||
MYSQL_USER: projectsend
|
||||
MYSQL_PASSWORD: change-me-database
|
||||
MYSQL_ROOT_PASSWORD: change-me-root
|
||||
volumes:
|
||||
- db-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
volumes:
|
||||
storage:
|
||||
db-data:
|
||||
redis-data:
|
||||
```
|
||||
|
||||
Then open `APP_URL` — the setup screen creates your administrator account. Uncomment
|
||||
`ADMIN_EMAIL` and `ADMIN_PASSWORD` above, with a password of your own, to create it unattended
|
||||
instead.
|
||||
|
||||
The first start is slower than later ones: the container waits for MySQL to accept connections
|
||||
before it migrates the database. That wait is normal, not a failure.
|
||||
|
||||
---
|
||||
|
||||
## Tags
|
||||
|
||||
| Tag | Moves |
|
||||
| --- | --- |
|
||||
| `2.1.0` | Never — an exact release |
|
||||
| `2.1` | With each patch on the 2.1 line |
|
||||
| `2` | With each release on the 2.x line |
|
||||
| `latest` | With each release |
|
||||
|
||||
Pin to `2` for unattended updates that stay on a compatible line, or to an exact version if you
|
||||
would rather decide when to move.
|
||||
|
||||
Built for **linux/amd64** and **linux/arm64**.
|
||||
|
||||
---
|
||||
|
||||
## What is in the image
|
||||
|
||||
One container runs the whole application under supervisord: **nginx**, **php-fpm**, the **queue
|
||||
worker** and the **scheduler**. You do not need a separate worker or a cron entry.
|
||||
|
||||
nginx is part of the image rather than an option, because protected downloads are served with
|
||||
`X-Accel-Redirect` — an fpm-only image would leave every download broken unless you reproduced the
|
||||
config exactly.
|
||||
|
||||
**You need to bring:**
|
||||
|
||||
- **MySQL 8.0 or newer** (we test on 8.4 LTS). Required.
|
||||
- **Redis** — recommended, but optional. Without it, set `CACHE_STORE`, `SESSION_DRIVER` and
|
||||
`QUEUE_CONNECTION` to `database` and drop the redis service; sessions, cache and jobs then live
|
||||
in MySQL.
|
||||
|
||||
The image packages the same release artifact as the published zip, byte for byte. It does not build
|
||||
the application at image build time.
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
Everything is read from the environment. The values below are the ones that matter; anything a
|
||||
Laravel application understands works too.
|
||||
|
||||
| Variable | Notes |
|
||||
| --- | --- |
|
||||
| `APP_URL` | **The address your users reach**, not the published port. Download links and password-reset emails are built from it. |
|
||||
| `APP_ENV` | `production` |
|
||||
| `APP_DEBUG` | `false` in production |
|
||||
| `APP_KEY` | Generated on first boot and kept on the storage volume. Set it explicitly only if you manage secrets elsewhere — and never change it on a running install: it decrypts existing data. |
|
||||
| `APP_TIMEZONE` | Defaults to `UTC` |
|
||||
| `TRUSTED_PROXIES` | Comma-separated addresses/CIDRs, or `*`. See below. |
|
||||
| `DB_HOST` `DB_PORT` `DB_DATABASE` `DB_USERNAME` `DB_PASSWORD` | MySQL connection |
|
||||
| `REDIS_HOST` `REDIS_PORT` `REDIS_PASSWORD` | Redis connection |
|
||||
| `CACHE_STORE` `SESSION_DRIVER` `QUEUE_CONNECTION` | `redis` or `database` |
|
||||
| `MAIL_MAILER` `MAIL_HOST` `MAIL_PORT` `MAIL_USERNAME` `MAIL_PASSWORD` `MAIL_ENCRYPTION` `MAIL_FROM_ADDRESS` | Fallback mail settings. Easier to configure from **System → Settings → Email** once you are signed in — it has a "send test" button. |
|
||||
| `FILESYSTEM_DISK` | `local` (default) or `s3` |
|
||||
| `AWS_ACCESS_KEY_ID` `AWS_SECRET_ACCESS_KEY` `AWS_DEFAULT_REGION` `AWS_BUCKET` `AWS_USE_PATH_STYLE_ENDPOINT` | For S3-compatible storage |
|
||||
| `ADMIN_NAME` `ADMIN_EMAIL` `ADMIN_PASSWORD` | Creates the first administrator unattended. Ignored once any user exists. |
|
||||
|
||||
### Behind a reverse proxy
|
||||
|
||||
Set `TRUSTED_PROXIES`. Without it every visitor appears to come from the proxy: the login rate
|
||||
limiter treats all of your users as one attacker, and the download log records the proxy's address
|
||||
instead of the real one. `*` is correct when nothing but your proxy can reach the container.
|
||||
|
||||
---
|
||||
|
||||
## Data and backups
|
||||
|
||||
Two things must outlive the container: **your MySQL database**, and the volume mounted at
|
||||
**`/var/www/html/storage`**. Uploaded files live on that volume, and so does the generated `.env`
|
||||
holding `APP_KEY` — losing the key makes the SMTP and LDAP passwords stored in your database
|
||||
undecryptable even if the rest of the backup is perfect.
|
||||
|
||||
**[DOCKER.md](https://github.com/projectsend/projectsend/blob/main/DOCKER.md) is the guide**: how
|
||||
to move both onto host paths you chose, the backup and restore commands, and how to move the whole
|
||||
installation to another server. Read it before you put real files in ProjectSend, not after.
|
||||
|
||||
---
|
||||
|
||||
## Updating
|
||||
|
||||
```sh
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The container migrates the database itself on boot — the same `php artisan projectsend:update` a
|
||||
manual install runs — so there is no separate step. Take a database dump first anyway: migrations
|
||||
move forwards, not backwards.
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
**For the people you send to**
|
||||
|
||||
- A private area per client, showing only what has been shared with them
|
||||
- Sign in with an email address, with optional two-factor authentication
|
||||
- Search, filter and sort; download one file, several as a zip, or a whole folder
|
||||
- Optional comments on a file, so questions live next to the thing they are about
|
||||
- Email notifications when something new arrives, in their own language
|
||||
|
||||
**For you**
|
||||
|
||||
- Resumable uploads that survive a dropped connection, so large files actually arrive
|
||||
- Folders, categories and client groups
|
||||
- Share with one client, a whole group, or publicly — with an expiry date or a download limit
|
||||
- Thumbnails and previews for images and documents
|
||||
- Storage quotas per client, and custom fields for the details you keep on them
|
||||
- A full activity log and download history: who got what, and when
|
||||
|
||||
**For the installation**
|
||||
|
||||
- Roles and permissions for your own team, so an uploader is not an administrator
|
||||
- LDAP, social sign-in, or plain email and password
|
||||
- Themes for the client-facing pages and for outgoing email
|
||||
- 16 languages
|
||||
- A REST API with scoped tokens and generated OpenAPI docs
|
||||
- Privacy controls, including GDPR-grade account erasure with a grace period
|
||||
- Local disk or S3-compatible storage
|
||||
|
||||
---
|
||||
|
||||
## Coming from ProjectSend Legacy?
|
||||
|
||||
The previous generation lives on at [projectsend/legacy](https://github.com/projectsend/legacy).
|
||||
This is a rebuild rather than an upgrade, so moving across is an import: install fresh, then bring
|
||||
your old site in with the [migration tool](https://github.com/projectsend/v1-migration-tool) —
|
||||
accounts, clients, groups, categories, folders, files and history. It never writes to your old
|
||||
install, and any run can be undone with a single command.
|
||||
|
||||
**[MIGRATING-FROM-V1.md](https://github.com/projectsend/projectsend/blob/main/MIGRATING-FROM-V1.md)
|
||||
is the guide**, and it has a section for this image specifically: the migration runs from the
|
||||
`projectsend:migrate:*` commands here rather than from a screen, because this image ships the
|
||||
application already built and carries no Composer of its own.
|
||||
|
||||
---
|
||||
|
||||
## Documentation and support
|
||||
|
||||
- **Source, issues and full docs:** [github.com/projectsend/projectsend](https://github.com/projectsend/projectsend)
|
||||
- **Docker guide:** [DOCKER.md](https://github.com/projectsend/projectsend/blob/main/DOCKER.md) — where your data lives, backups, moving to another server
|
||||
- **Updating:** [UPDATE.md](https://github.com/projectsend/projectsend/blob/main/UPDATE.md)
|
||||
- **Installing without Docker:** [INSTALL.md](https://github.com/projectsend/projectsend/blob/main/INSTALL.md)
|
||||
- **Chat:** [Discord](https://discord.gg/VT9n6cyvXT)
|
||||
|
||||
Found a security issue? Please report it privately through GitHub's security advisories rather than
|
||||
opening a public issue.
|
||||
|
||||
## License
|
||||
|
||||
GNU General Public License v2, or (at your option) any later version. Commercial licenses are
|
||||
available for organizations that cannot work under copyleft terms — see
|
||||
[LICENSING.md](https://github.com/projectsend/projectsend/blob/main/LICENSING.md).
|
||||
@@ -40,7 +40,15 @@ if [ ! -f storage/.env ]; then
|
||||
cp .env.example storage/.env
|
||||
chown www-data:www-data storage/.env
|
||||
fi
|
||||
#
|
||||
# The symlink is owned by the runtime user, not by root who creates it:
|
||||
# fs.protected_symlinks lets a process follow a symlink in a world-writable
|
||||
# sticky directory only when it owns the link (or the directory). The image
|
||||
# no longer leaves /var/www/html in that state, so this is the second lock
|
||||
# on the same door — cheap, and it is what keeps the failure from coming
|
||||
# back silently if that directory's mode ever drifts.
|
||||
[ -L .env ] || ln -sf storage/.env .env
|
||||
chown -h www-data:www-data .env
|
||||
|
||||
if [ -z "$APP_KEY" ] && ! grep -q '^APP_KEY=base64:' storage/.env; then
|
||||
echo "projectsend: generating APP_KEY (first boot, persisted to the storage volume)"
|
||||
@@ -62,9 +70,13 @@ if [ "$1" = "supervisord" ] || [ "$1" = "/usr/bin/supervisord" ]; then
|
||||
sleep 1
|
||||
done
|
||||
|
||||
su-exec www-data php artisan migrate --force
|
||||
su-exec www-data php artisan storage:link --force
|
||||
su-exec www-data php artisan projectsend:ensure-roles
|
||||
# One command, shared with update.sh and with the dev image: migrate,
|
||||
# ensure the roles, relink storage, clear the compiled caches, restart
|
||||
# the workers, record the version applied. See UpdateInstallation for
|
||||
# why the order is what it is, and why maintenance mode is not part of
|
||||
# it (`artisan down` would write its flag onto this container's
|
||||
# persisted storage volume and outlive the container that wrote it).
|
||||
su-exec www-data php artisan projectsend:update
|
||||
|
||||
# Unattended provisioning: create the first administrator from the
|
||||
# environment. Without these, the web setup screen prompts instead.
|
||||
@@ -74,10 +86,6 @@ if [ "$1" = "supervisord" ] || [ "$1" = "/usr/bin/supervisord" ]; then
|
||||
--email="$ADMIN_EMAIL" \
|
||||
--password="$ADMIN_PASSWORD"
|
||||
fi
|
||||
|
||||
# Cheap and safe to repeat; keeps a restarted container from serving
|
||||
# stale compiled views after an image upgrade.
|
||||
su-exec www-data php artisan view:clear
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
# being one container instead of two:
|
||||
#
|
||||
# 1. fastcgi_pass targets 127.0.0.1, not the `app` compose service.
|
||||
# 2. There is no user/uid coordination to do: nginx and php-fpm run as
|
||||
# the same www-data here, so storage/ needs no group juggling.
|
||||
# 2. There is no user/uid coordination to do here, because the Dockerfile
|
||||
# has already done it: it points the `user` directive in the package's
|
||||
# own nginx.conf at www-data, so nginx and php-fpm are the same user
|
||||
# and storage/ needs no group juggling. Without that step nginx runs
|
||||
# as the alpine package's `nginx` (uid 100) and cannot read what
|
||||
# php-fpm just wrote — see the comment on that line.
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
@@ -46,7 +50,10 @@ server {
|
||||
# cannot touch this app's origin or the viewer's session. Images loaded
|
||||
# as subresources are unaffected: a CSP on a subresource response never
|
||||
# creates a browsing context, so thumbnails and previews still render.
|
||||
location /protected-files/ {
|
||||
# `^~` so this prefix beats the `\.php$` regex below: without it a
|
||||
# protected path ending in .php would be handed to the PHP handler
|
||||
# instead of streaming under the sandbox headers this block sets.
|
||||
location ^~ /protected-files/ {
|
||||
internal;
|
||||
alias /var/www/html/storage/app/files/;
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ server {
|
||||
# cannot touch this app's origin or the viewer's session. Images loaded
|
||||
# as subresources are unaffected: a CSP on a subresource response never
|
||||
# creates a browsing context, so thumbnails and previews still render.
|
||||
location /protected-files/ {
|
||||
# `^~` so this prefix beats the `\.php$` regex below: without it a
|
||||
# protected path ending in .php would be handed to the PHP handler
|
||||
# instead of streaming under the sandbox headers this block sets.
|
||||
location ^~ /protected-files/ {
|
||||
internal;
|
||||
alias /var/www/html/storage/app/files/;
|
||||
|
||||
|
||||
+4
-1
@@ -41,7 +41,10 @@ export default [
|
||||
// Maintainer tooling scripts (*.cjs) are plain Node, not app
|
||||
// source — they don't run in the browser and use Node globals
|
||||
// (require, process, __dirname) the app's ruleset forbids.
|
||||
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js', '.claude', 'packages'],
|
||||
// `.release-build` is where build-release.sh assembles a zip: a full
|
||||
// copy of the app plus vendored, minified JS. It is gitignored, so
|
||||
// linting it only ever means `--fix` rewriting artifacts nobody reads.
|
||||
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js', '.claude', 'packages', '.release-build'],
|
||||
},
|
||||
prettier, // Turn off all rules that might conflict with Prettier
|
||||
];
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count descàrregues",
|
||||
":count file deleted.|:count files deleted.": ":count fitxer eliminat.|:count fitxers eliminats.",
|
||||
":count file imported.|:count files imported.": ":count fitxer importat.|:count fitxers importats.",
|
||||
":count files": ":count fitxers",
|
||||
":count folders": ":count carpetes",
|
||||
":count items have been shared with you": "S'han compartit :count elements amb tu",
|
||||
":count members": ":count membres",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(i :count error més)",
|
||||
"(and :count more errors)": "(i :count error més)|(i :count errors més)|(i :count errors més)",
|
||||
"(deleted account)": "(compte eliminat)",
|
||||
"0 means unlimited.": "0 vol dir il·limitat.",
|
||||
"A client account request was approved": "S'ha aprovat una sol·licitud de compte de client",
|
||||
"A client account request was denied": "S'ha denegat una sol·licitud de compte de client",
|
||||
"A client left a group": "Un client ha deixat un grup",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "S'ha creat un compte de client nou: :name (:email).",
|
||||
"A new client has registered": "S'ha registrat un client nou",
|
||||
"A new ProjectSend version is available": "Hi ha disponible una versió nova del ProjectSend",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "La pàgina d'un grup públic sempre és accessible pel seu propi enllaç un cop el marques com a públic; aquesta secció només controla l'URL base i un directori opcional que mostra alhora tots els grups i fitxers públics.",
|
||||
"A public link was created": "S'ha creat un enllaç públic",
|
||||
"A public link was revoked": "S'ha revocat un enllaç públic",
|
||||
"A role was created": "S'ha creat un rol",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Administració: s'ha sol·licitat l'adhesió a un grup",
|
||||
"Admin: new client registered": "Administració: s'ha registrat un client nou",
|
||||
"Administration": "Administració",
|
||||
"After uploading you can assign it to clients and groups": "Un cop pujat, el podràs assignar a clients i grups",
|
||||
"Agree": "Acordar",
|
||||
"All": "Tots",
|
||||
"All actions": "Totes les accions",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Tots els comentaris",
|
||||
"All failed jobs deleted.": "S'han esborrat totes les tasques fallides.",
|
||||
"All files": "Tots els fitxers",
|
||||
"All groups": "Tots els grups",
|
||||
"All permissions": "Tots els permisos",
|
||||
"All rights reserved.": "Tots els drets reservats.",
|
||||
"All roles": "Tots els rols",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Qui l'utilitzi ja no podrà descarregar el fitxer.",
|
||||
"Appearance": "Aparença",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "S'aplica a les pàgines públiques de grups i fitxers per a visitants i al portal de clients «Els meus fitxers».",
|
||||
"Apply filters": "Aplica els filtres",
|
||||
"Approve": "Aprova",
|
||||
"Approve account requests": "Aprovar sol·licituds de compte",
|
||||
"Approve group membership requests": "Aprovar sol·licituds d'adhesió a grups",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Segur que vols eliminar el teu compte?",
|
||||
"Assign": "Assignar",
|
||||
"Assign categories to files": "Assignar categories als fitxers",
|
||||
"Assigned": "Assignat",
|
||||
"Assigned clients": "Clients assignats",
|
||||
"Assigned the file \":subject\" to :target": "Ha assignat el fitxer \":subject\" a :target",
|
||||
"Associate": "Associat",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Autor",
|
||||
"Available groups": "Grups disponibles",
|
||||
"Available placeholders": "Marcadors disponibles",
|
||||
"Back to My files": "Torna a Els meus fitxers",
|
||||
"Bad Gateway": "Porta d'enllaç incorrecta",
|
||||
"Bad Request": "Petició dolenta",
|
||||
"Bandwidth Limit Exceeded": "Límit d'ample de banda excedit",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Cos del missatge",
|
||||
"Browse": "Navega",
|
||||
"Browse publicly shared groups and files below.": "Explora a sota els grups i fitxers compartits públicament.",
|
||||
"Built-in": "Integrat",
|
||||
"Built-in role names cannot be changed.": "El nom dels rols integrats no es pot canviar.",
|
||||
"Built-in roles cannot be deleted.": "Els rols integrats no es poden eliminar.",
|
||||
"Call to action": "Crida a l'acció",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Confirma la contrasenya",
|
||||
"Conflict": "Conflicte",
|
||||
"Connect": "Connecta't",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Connecta un bucket extern — compatible amb S3 (AWS S3, MinIO, Backblaze) o Google Cloud Storage — com a emmagatzematge de les càrregues noves.",
|
||||
"Connection Closed Without Response": "Connexió tancada sense resposta",
|
||||
"Connection Timed Out": "Temps de connexió esgotat",
|
||||
"Continue": "Continua",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Desactiva el teu compte ara; s'esborra definitivament al cap de :days dies",
|
||||
"Deactivated the account \":subject\"": "Ha desactivat el compte \":subject\"",
|
||||
"Default": "Per defecte",
|
||||
"Defaults to the file name.": "Per defecte, el nom del fitxer.",
|
||||
"Delete": "Esborrar",
|
||||
"Delete :name": "Suprimeix :name",
|
||||
"Delete account": "Elimina el compte",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Denega",
|
||||
"Deny this request?": "Vols denegar aquesta sol·licitud?",
|
||||
"Description": "Descripció",
|
||||
"Description (optional)": "Descripció (opcional)",
|
||||
"Detach": "Separa",
|
||||
"Details": "Detalls",
|
||||
"Disable": "Deshabilitar",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Aneu a la pàgina :page",
|
||||
"Go to the login form": "Ves al formulari d'inici de sessió",
|
||||
"Gone": "Desaparegut",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Gris",
|
||||
"Green": "Verd",
|
||||
"Group": "Grup",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Entrar",
|
||||
"Logout": "Sortir",
|
||||
"Loop Detected": "Bucle detectat",
|
||||
"Mail provider settings saved.": "Configuració del proveïdor de correu desada.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "mode de manteniment",
|
||||
"Manage custom fields": "Gestionar camps personalitzats",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Encara no s'ha compartit cap fitxer amb tu.",
|
||||
"No files here yet.": "Encara no hi ha cap fitxer aquí.",
|
||||
"No files or folders match your search.": "Cap fitxer ni carpeta coincideix amb la teva cerca.",
|
||||
"No files yet. Upload the first one.": "Encara no hi ha fitxers. Puja'n el primer.",
|
||||
"No folder": "Cap carpeta",
|
||||
"No groups match these filters.": "Cap grup coincideix amb aquests filtres.",
|
||||
"No groups yet.": "Encara no hi ha grups.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Encara no s'ha afegit cap destinatari.",
|
||||
"No requests match your search.": "Cap sol·licitud coincideix amb la teva cerca.",
|
||||
"No roles match this filter.": "Cap rol coincideix amb aquest filtre.",
|
||||
"No service account key has been saved yet.": "Encara no s'ha desat cap clau de compte de servei.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Sense límit de mida. Les pujades es poden posar en pausa i es reprenen automàticament després d'una interrupció.",
|
||||
"No users match these filters.": "Cap usuari coincideix amb aquests filtres.",
|
||||
"No users yet.": "Encara no hi ha usuaris.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Taronja",
|
||||
"Origin Is Unreachable": "L'origen és inabastable",
|
||||
"Outgoing email": "Correu de sortida",
|
||||
"Outgoing email theme": "Tema del correu de sortida",
|
||||
"Page :current of :last": "Pàgina :current de :last",
|
||||
"Page Expired": "Pàgina caducada",
|
||||
"Pagination Navigation": "Navegació de la paginació",
|
||||
"Partial Content": "Contingut parcial",
|
||||
"Password": "Contrasenya",
|
||||
"Password reset": "Restabliment de la contrasenya",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Enganxa el fitxer de clau JSON d'un compte de servei amb accés de lectura i escriptura als objectes del bucket. Es desa xifrat i no es torna a mostrar.",
|
||||
"Path": "Camí",
|
||||
"Payload Too Large": "Càrrega útil massa gran",
|
||||
"Payment Required": "Pagament obligatori",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Configuració del llistat públic",
|
||||
"Public listing URL": "URL del llistat públic",
|
||||
"Public pages & portal": "Pàgines públiques i portal",
|
||||
"Public pages and client portal theme": "Tema de les pàgines públiques i del portal de clients",
|
||||
"Published": "Publicat",
|
||||
"Purple": "Lila",
|
||||
"Quota": "Quota",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Restauració",
|
||||
"Restore :name": "Restaurar :name",
|
||||
"Restricted extension": "Extensió restringida",
|
||||
"Result": "Resultat",
|
||||
"results": "resultats",
|
||||
"Review membership requests": "Revisar sol·licituds d'adhesió",
|
||||
"Review pending requests": "Revisa les sol·licituds pendents",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Nom del rol",
|
||||
"Role updated.": "Rol actualitzat.",
|
||||
"Roles": "Rols",
|
||||
"S3-compatible": "Compatible amb S3",
|
||||
"Sample email": "Correu d'exemple",
|
||||
"Save": "Desar",
|
||||
"Save :name": "Estalvia :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Envia ara mateix un correu de debò, sense passar per la cua, perquè puguis comprovar que la configuració de sobre funciona realment.",
|
||||
"Separate paragraphs with a blank line.": "Separa els paràgrafs amb una línia en blanc.",
|
||||
"Server Error": "Error del servidor",
|
||||
"Service account key": "Clau de compte de servei",
|
||||
"Service Unavailable": "Servei no disponible",
|
||||
"Session Has Expired": "La sessió ha caducat",
|
||||
"Set file expiration dates": "Establir dates de caducitat dels fitxers",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Configuració",
|
||||
"Settings that apply to the whole installation": "Configuració que s'aplica a tota la instal·lació",
|
||||
"Setup complete": "Configuració completada",
|
||||
"Share a file with the people who manage your account.": "Comparteix un fitxer amb les persones que gestionen el teu compte.",
|
||||
"Share files with several clients at once": "Comparteix fitxers amb diversos clients alhora",
|
||||
"Shared the folder \":subject\" with :target": "Ha compartit la carpeta \":subject\" amb :target",
|
||||
"Shared with": "Compartit amb",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Protocols de commutació",
|
||||
"System": "Sistema",
|
||||
"System Administrator": "Administrador del sistema",
|
||||
"System settings": "Configuració del sistema",
|
||||
"System settings were updated": "S'ha actualitzat la configuració del sistema",
|
||||
"System users": "Usuaris del sistema",
|
||||
"Tag": "Etiqueta",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Prova",
|
||||
"Text": "Text",
|
||||
"Text or author": "Text o autor",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Això no sembla un fitxer de clau de compte de servei: no és JSON vàlid.",
|
||||
"That service account key file is missing its :field.": "A aquest fitxer de clau de compte de servei li falta :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "El compte de :name s'eliminarà definitivament. Aquesta acció no es pot desfer.",
|
||||
"The account password was changed": "S'ha canviat la contrasenya del compte",
|
||||
"The account request of :name will be denied and the account deleted.": "La sol·licitud de compte de :name es denegarà i el compte s'eliminarà.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Els grups als quals pertanys i aquells als quals pots demanar entrar",
|
||||
"The header/footer style used for every outgoing notification email.": "L'estil de capçalera i peu que es fa servir a cada correu de notificació de sortida.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "L'aspecte de les pàgines públiques per a visitants, del portal de clients i dels correus de notificació de sortida.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "El servidor de correu es configura al fitxer .env d'aquesta instal·lació (variables MAIL_*), no pas aquí.",
|
||||
"The people who administer this installation and upload files": "Les persones que administren aquesta instal·lació i pugen fitxers",
|
||||
"The people you share files with": "Les persones amb qui comparteixes fitxers",
|
||||
"The provided two-factor authentication code was invalid.": "El codi d'autenticació de dos factors que has introduït no és vàlid.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Encara no hi ha res per configurar.",
|
||||
"Test email from :site": "Correu de prova de :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Aquests fitxers s'eliminaran definitivament del disc. Aquesta acció no es pot desfer.",
|
||||
"Theme": "Tema",
|
||||
"Theme updated.": "Tema actualitzat.",
|
||||
"Theming": "Temes",
|
||||
"Theming settings": "Configuració dels temes",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Aquesta pujada superaria la teva quota d'emmagatzematge de :quota MB.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Consell: arrossega fitxers i carpetes sobre una carpeta o sobre el rastre de navegació per moure'ls.",
|
||||
"Title": "Títol",
|
||||
"Title (optional)": "Títol (opcional)",
|
||||
"TLS": "TLS",
|
||||
"to": "a",
|
||||
"To": "Per a",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Hi ha massa fitxers seleccionats. Redueix la selecció i torna-ho a provar.",
|
||||
"Too Many Requests": "Massa peticions",
|
||||
"Transactional email settings for this installation": "Configuració dels correus transaccionals d'aquesta instal·lació",
|
||||
"Transfers — last 30 days": "Transferències — últims 30 dies",
|
||||
"Translate": "Traduir",
|
||||
"Translate It": "Tradueix-ho",
|
||||
"Turns all transactional email for this installation on or off.": "Activa o desactiva tots els correus transaccionals d'aquesta instal·lació.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Ha pujat el fitxer \":subject\"",
|
||||
"Uploader": "Autor de la pujada",
|
||||
"Uploads": "Pujades",
|
||||
"Uploads (chart)": "Pujades (gràfic)",
|
||||
"URI Too Long": "URI massa llarg",
|
||||
"URL slug": "Sufix d'URL",
|
||||
"Use a custom link instead of a random one": "Fes servir un enllaç personalitzat en comptes d'un d'aleatori",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Aquesta és l'única vegada que es mostrarà. Només en desem un resum criptogràfic, de manera que no es podrà recuperar més endavant.",
|
||||
"Copy to clipboard": "Copia al porta-retalls",
|
||||
"Copied": "Copiat",
|
||||
"Your tokens": "Els teus testimonis",
|
||||
"Revoking a token takes effect immediately.": "La revocació d'un testimoni té efecte immediat.",
|
||||
"You have not created any tokens yet.": "Encara no has creat cap testimoni.",
|
||||
"Expired": "Caducat",
|
||||
"Never used": "No s'ha fet servir mai",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Gestionar camps personalitzats de clients",
|
||||
"Moderate comments": "Moderar comentaris",
|
||||
"Only me": "Només jo",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Visible per a qualsevol persona que pugui obrir aquest fitxer, inclosos els visitants que no han iniciat la sessió.",
|
||||
"Anyone, including visitors who are not logged in": "Tothom, inclosos els visitants que no han iniciat la sessió",
|
||||
"Staff and clients": "Equip i clients",
|
||||
"Staff only": "Només l'equip",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Els comentaris ja escrits continuen sent llegibles encara que un fitxer deixi d'acceptar-ne de nous.",
|
||||
"Comments awaiting approval": "Comentaris que esperen aprovació",
|
||||
"Comment settings": "Configuració dels comentaris",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Comentaris deixats per visitants que no han iniciat la sessió. Ningú no els pot veure fins que els aprovis.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Connecta un bucket extern compatible amb S3 (AWS S3, MinIO o un altre servei compatible) com a emmagatzematge de les pujades noves.",
|
||||
"Custom endpoint": "Punt d'accés personalitzat",
|
||||
"Custom field created.": "Camp personalitzat creat.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Cap tasca fallida.",
|
||||
"No files match these filters.": "Cap fitxer coincideix amb aquests filtres.",
|
||||
"None of the selected files could be edited.": "No s'ha pogut editar cap dels fitxers seleccionats.",
|
||||
"Nothing is waiting for approval.": "No hi ha res esperant aprovació.",
|
||||
"of :count": "de :count",
|
||||
"of :quota": "de :quota",
|
||||
"on": "el",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Pàgina",
|
||||
"Path prefix": "Prefix del camí",
|
||||
"Pending": "Pendent",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Comprova periòdicament si el dipòsit públic de projectsend.org té una versió més nova i mostra un avís al tauler. No es descarrega ni s'aplica mai res automàticament.",
|
||||
"Permanently failed": "Ha fallat definitivament",
|
||||
"Post comment": "Publica el comentari",
|
||||
"Preview of :name": "Vista prèvia de :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Elimina la verificació en dos passos",
|
||||
"Remove two-factor authentication?": "Vols eliminar la verificació en dos passos?",
|
||||
"Require a verified email address": "Exigeix una adreça de correu verificada",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Executa-ho com l'usuari amb què s'executa el teu servidor web. INSTALL.md té el procediment complet.",
|
||||
"Score threshold": "Llindar de puntuació",
|
||||
"Search timezones…": "Cerca zones horàries…",
|
||||
"Security check failed. Please try again.": "La comprovació de seguretat ha fallat. Torna-ho a provar.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Aquest lloc està protegit per reCAPTCHA i s'apliquen la Política de privadesa i les Condicions del servei de Google.",
|
||||
"Those files have reached their download limit.": "Aquests fitxers han assolit el seu límit de baixades.",
|
||||
"Timezone": "Zona horària",
|
||||
"To update, back up your database and files, then:": "Per actualitzar, fes una còpia de seguretat de la teva base de dades i els teus fitxers, i després:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Per actualitzar, segueix els passos d'INSTALL.md: primer fes una còpia de seguretat, després descomprimeix la versió i executa les migracions.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Desactiva això només si aquest servidor no ha de fer cap petició de sortida. Si no es pot contactar amb el servei, la contrasenya s'accepta igualment, així que una instal·lació sense connexió mai no queda bloquejada per això.",
|
||||
"Two-factor authentication removed.": "Verificació en dos passos eliminada.",
|
||||
"Two-factor authentication removed by an admin": "Verificació en dos passos eliminada per un administrador",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "El teu compte està en :timezone, així que això no canviarà com veus tu les dates.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "El teu propi inquilí, no «common». Microsoft permet que un usuari canviï l'adreça de correu del seu compte, així que un inici de sessió només és fiable quan el testimoni prové de l'inquilí que has indicat aquí.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "El teu proveïdor ha rebutjat la clau secreta d'aquesta instal·lació, així que els formularis protegits es deixen passar sense comprovar. Revisa la clau secreta de sota.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Els teus formularis públics estan protegits. Pots canviar a les teves pròpies claus en qualsevol moment."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Els teus formularis públics estan protegits. Pots canviar a les teves pròpies claus en qualsevol moment.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Si has tornat enrere a propòsit, executa php artisan projectsend:update perquè la base de dades coincideixi amb el codi que has restaurat.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Pregunta abans de buscar una versió, abans de descarregar-la i abans de tocar la teva instal·lació, i verifica la suma de verificació del que descarrega. UPDATE.md té el procediment complet.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Els fitxers nous ja hi són, però la base de dades només s'ha actualitzat fins a :applied. Fins que no executis l'actualització, aquesta instal·lació funciona amb codi que no coincideix amb la seva base de dades.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Comprova periòdicament si el dipòsit públic de projectsend.org té una versió més nova i mostra un avís al tauler. No es descarrega ni s'aplica mai res automàticament: actualitzar sempre és una cosa que fas tu, i l'script d'actualització pregunta abans de cada pas.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "La base de dades s'ha actualitzat a :applied, i PHP encara serveix el codi que tenia compilat abans. Si el recarregues, aquest avís desapareix.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "La base de dades s'ha actualitzat a :applied, i aquest contenidor encara executa el codi amb què va arrencar.",
|
||||
"This server is running version :running, but version :applied is installed": "Aquest servidor executa la versió :running, però la versió instal·lada és :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Per actualitzar, executa sudo ./update.sh al directori d'instal·lació: pregunta abans de descarregar o canviar res.",
|
||||
"To update, run this in the install directory:": "Per actualitzar, executa això al directori d'instal·lació:",
|
||||
"Version :running is installed, but the update has not been run": "La versió :running està instal·lada, però l'actualització no s'ha executat",
|
||||
"Updated ProjectSend to :to, from :from": "Ha actualitzat el ProjectSend a :to, des de :from",
|
||||
"ProjectSend was updated to a new version": "S'ha actualitzat el ProjectSend a una versió nova",
|
||||
"Come and say hello": "Vine a saludar-nos",
|
||||
"Continue to the dashboard": "Continua al tauler",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Uneix-te al Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "El ProjectSend té un Discord: novetats de cada versió, ajuda quan alguna cosa no va com hauria i altres persones que fan servir el mateix programari. Nosaltres també hi som.",
|
||||
"What's new": "Novetats",
|
||||
"What each release brought to this installation.": "Què ha aportat cada versió a aquesta instal·lació.",
|
||||
"Add the rest of your team": "Afegeix la resta del teu equip",
|
||||
"Add your first client": "Afegeix el teu primer client",
|
||||
"Check the scheduler is running": "Comprova que el planificador funciona",
|
||||
"Choose how your email looks": "Tria com es veu el teu correu",
|
||||
"Choose how your file lists look": "Tria com es veuen les teves llistes de fitxers",
|
||||
"Drop one in and choose who gets it.": "Puja'n un i tria qui el rep.",
|
||||
"Essential": "Imprescindible",
|
||||
"Expiring files and queued email need it.": "Els fitxers que caduquen i el correu en cua en depenen.",
|
||||
"Four layouts for the client-facing pages.": "Quatre dissenys per a les pàgines que veuen els clients.",
|
||||
"Getting started": "Primers passos",
|
||||
"Group your clients": "Agrupa els teus clients",
|
||||
"Made with care in Argentina. Shared with the world.": "Fet amb cura a l'Argentina. Compartit amb el món.",
|
||||
"Previewed on a real message.": "Previsualitzat en un missatge real.",
|
||||
"Resets and share links go out through it.": "Els restabliments i els enllaços compartits hi passen.",
|
||||
"Set up your mail server": "Configura el teu servidor de correu",
|
||||
"Share once instead of six times.": "Comparteix un cop en lloc de sis.",
|
||||
"Staff accounts, each scoped by its role.": "Comptes d'equip, cadascun limitat pel seu rol.",
|
||||
"Thank you for installing ProjectSend": "Gràcies per instal·lar el ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Gràcies per actualitzar el ProjectSend",
|
||||
"The people you send files to.": "Les persones a qui envies fitxers.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "La versió curta del que cal fer en una instal·lació nova. Res d'això caduca.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Ara estàs a la :version i tot ha tornat a funcionar. Gràcies per continuar confiant en el ProjectSend per compartir els teus fitxers.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Ara estàs a la :version, des de la :previous, i tot ha tornat a funcionar. Gràcies per continuar confiant en el ProjectSend per compartir els teus fitxers.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Estàs fent servir la versió :version, i és teva. Això és el que convé fer primer.",
|
||||
"Thank you for choosing ProjectSend": "Gràcies per triar el ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "El teu lloc està a punt, i mantenir-lo en marxa és feina nostra. Això és el que convé fer primer.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 ho conserva tot. Les notificacions no llegides no s'eliminen mai, per antigues que siguin.",
|
||||
":version is available": ":version està disponible",
|
||||
":version on :date": ":version el :date",
|
||||
"Check now": "Comprova ara",
|
||||
"Checked a moment ago, so this is the answer from then.": "La comprovació és de fa un moment, així que aquesta és la resposta d'aleshores.",
|
||||
"Checking…": "Comprovant…",
|
||||
"Could not reach the update feed.": "No s'ha pogut accedir al canal d'actualitzacions.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Fes un bolcat de la base de dades abans de tocar res, i atura't si el bolcat falla.",
|
||||
"Housekeeping": "Manteniment",
|
||||
"Keep failed jobs for": "Conserva les tasques fallides durant",
|
||||
"Keep read notifications for": "Conserva les notificacions llegides durant",
|
||||
"Last checked :date": "Última comprovació: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "No toquis el PHP ni el worker, per quan els reiniciïs tu mateix.",
|
||||
"Other options": "Altres opcions",
|
||||
"Report which version is installed and which is available, and change nothing.": "Informa de quina versió hi ha instal·lada i quina està disponible, i no canvia res.",
|
||||
"Retention updated.": "Retenció actualitzada.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Si executes l'script sense opcions, et pregunta cadascuna d'aquestes coses sobre la marxa. Aquestes són per a les execucions en què ja saps la resposta.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Omet la pregunta sobre les còpies de seguretat, per a una execució que no estàs supervisant.",
|
||||
"The latest release (:tag) is not a version this can compare.": "La darrera versió publicada (:tag) no és una versió que es pugui comparar aquí.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Dues coses s'acumulen soles. Aquests són els terminis a què s'ajusten les purgues diàries de dalt.",
|
||||
"Up to date": "Al dia",
|
||||
"Update checks are not available on this edition.": "Les comprovacions d'actualització no estan disponibles en aquesta edició.",
|
||||
"Update options": "Opcions d'actualització",
|
||||
"Updated": "Actualitzat",
|
||||
"Version :version is available. You are running :current.": "La versió :version està disponible. Estàs fent servir la :current.",
|
||||
"You are up to date, running version :current.": "Estàs al dia, amb la versió :current.",
|
||||
"days": "dies",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help llista totes les opcions, i UPDATE.md explica tot el procediment, incloent-hi fer-ho a mà.",
|
||||
"Fetch dashboard news": "Obtén novetats del tauler",
|
||||
"Purge API request logs": "Purga el registre de peticions d'API",
|
||||
"Purge erased accounts": "Purga els comptes esborrats",
|
||||
"Purge expired files": "Purga els fitxers caducats",
|
||||
"Purge failed jobs": "Purga les tasques fallides",
|
||||
"Purge orphan files": "Purga els fitxers orfes",
|
||||
"Purge read notifications": "Purga les notificacions llegides",
|
||||
"Purge stale chunked uploads": "Purga les pujades per parts abandonades",
|
||||
"Purge zip downloads": "Purga les descàrregues ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Coincidències amb aquest filtre: :count, de la més recent a la més antiga",
|
||||
"A file was previewed via the public group listing": "S'ha previsualitzat un fitxer mitjançant el llistat públic de grups",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Qualsevol persona amb l'enllaç pot obrir una imatge, un vídeo, un fitxer d'àudio o un PDF al navegador en comptes de descarregar-lo. Desactiva-ho perquè descarregar sigui l'única manera de veure un fitxer públic.",
|
||||
"Clients can preview files": "Els clients poden previsualitzar fitxers",
|
||||
"Downloads & previews": "Descàrregues i vistes prèvies",
|
||||
"Downloads: :count": "Descàrregues: :count",
|
||||
"Let visitors preview public files": "Permet que els visitants previsualitzin fitxers públics",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Permet que els clients obrin una imatge, un vídeo, un fitxer d'àudio o un PDF al portal en comptes de només descarregar-lo. Desactiva-ho perquè descarregar sigui l'única manera de veure un fitxer. L'equip sempre pot previsualitzar.",
|
||||
"No downloads match these filters.": "Cap descàrrega coincideix amb aquests filtres.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Encara ningú no ha descarregat ni previsualitzat aquest fitxer.",
|
||||
"Preview :name": "Previsualitza :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Ha previsualitzat el fitxer \":subject\" mitjançant el llistat públic de grups",
|
||||
"Previews: :count": "Vistes prèvies: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Aquesta instal·lació construeix les seves pròpies imatges, així que baixar-ne una no canvia res. Si la versió ha modificat composer.lock o el frontend, executa també composer install i npm run build: UPDATE.md té el procediment complet.",
|
||||
"To update, run this in the directory you cloned:": "Per actualitzar, executa això al directori que has clonat:",
|
||||
"View all previews (:count)": "Mostra totes les vistes prèvies (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "El navegador no pot reproduir aquest fitxer. Descarrega'l per veure'l."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": "Stažení: :count",
|
||||
":count file deleted.|:count files deleted.": "Smazán :count soubor.|Smazáno :count souborů.",
|
||||
":count file imported.|:count files imported.": "Naimportován :count soubor.|Naimportováno :count souborů.",
|
||||
":count files": "Souborů: :count",
|
||||
":count folders": "Složek: :count",
|
||||
":count items have been shared with you": "Počet položek sdílených s vámi: :count",
|
||||
":count members": "Členů: :count",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(a :count dalších chyb)",
|
||||
"(and :count more errors)": "(a :count dalších chyb)|(a :count dalších chyb)|(a :count dalších chyb)",
|
||||
"(deleted account)": "(smazaný účet)",
|
||||
"0 means unlimited.": "0 znamená bez omezení.",
|
||||
"A client account request was approved": "Žádost o klientský účet byla schválena",
|
||||
"A client account request was denied": "Žádost o klientský účet byla zamítnuta",
|
||||
"A client left a group": "Klient opustil skupinu",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Byl vytvořen nový klientský účet: :name (:email).",
|
||||
"A new client has registered": "Zaregistroval se nový klient",
|
||||
"A new ProjectSend version is available": "Je k dispozici nová verze ProjectSend",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Stránka veřejné skupiny je vždy dostupná přes vlastní odkaz, jakmile ji označíte jako veřejnou — tato sekce řídí pouze základní URL a volitelný rozcestník, který naráz vypisuje všechny veřejné skupiny a soubory.",
|
||||
"A public link was created": "Byl vytvořen veřejný odkaz",
|
||||
"A public link was revoked": "Veřejný odkaz byl zneplatněn",
|
||||
"A role was created": "Role byla vytvořena",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Administrace: požádáno o členství ve skupině",
|
||||
"Admin: new client registered": "Administrace: zaregistroval se nový klient",
|
||||
"Administration": "Administrace",
|
||||
"After uploading you can assign it to clients and groups": "Po nahrání jej můžete přiřadit klientům a skupinám",
|
||||
"Agree": "Souhlasit",
|
||||
"All": "Vše",
|
||||
"All actions": "Všechny akce",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Všechny komentáře",
|
||||
"All failed jobs deleted.": "Všechny neúspěšné úlohy byly odstraněny.",
|
||||
"All files": "Všechny soubory",
|
||||
"All groups": "Všechny skupiny",
|
||||
"All permissions": "Všechna oprávnění",
|
||||
"All rights reserved.": "Všechna práva vyhrazena.",
|
||||
"All roles": "Všechny role",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Kdo jej používá, už soubor nestáhne.",
|
||||
"Appearance": "Vzhled",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Platí pro veřejné stránky skupin a souborů určené návštěvníkům a pro klientský portál „Moje soubory“.",
|
||||
"Apply filters": "Použít filtry",
|
||||
"Approve": "Schválit",
|
||||
"Approve account requests": "Schvalování žádostí o účet",
|
||||
"Approve group membership requests": "Schvalování žádostí o členství ve skupinách",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Opravdu chcete smazat svůj účet?",
|
||||
"Assign": "Přiřadit",
|
||||
"Assign categories to files": "Přiřazování kategorií k souborům",
|
||||
"Assigned": "Přiřazeno",
|
||||
"Assigned clients": "Přiřazení klienti",
|
||||
"Assigned the file \":subject\" to :target": "Přiřadil(a) soubor \":subject\" k :target",
|
||||
"Associate": "Spolupracovník",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Autor",
|
||||
"Available groups": "Dostupné skupiny",
|
||||
"Available placeholders": "Dostupné zástupné výrazy",
|
||||
"Back to My files": "Zpět na Moje soubory",
|
||||
"Bad Gateway": "Špatná brána",
|
||||
"Bad Request": "Špatný požadavek",
|
||||
"Bandwidth Limit Exceeded": "Překročen limit šířky pásma",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Text zprávy",
|
||||
"Browse": "Procházet",
|
||||
"Browse publicly shared groups and files below.": "Níže si můžete projít veřejně sdílené skupiny a soubory.",
|
||||
"Built-in": "Vestavěná",
|
||||
"Built-in role names cannot be changed.": "Názvy vestavěných rolí nelze měnit.",
|
||||
"Built-in roles cannot be deleted.": "Vestavěné role nelze mazat.",
|
||||
"Call to action": "Výzva k akci",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Potvrzení hesla",
|
||||
"Conflict": "Konflikt",
|
||||
"Connect": "Připojit",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Připojte externí bucket — kompatibilní s S3 (AWS S3, MinIO, Backblaze) nebo Google Cloud Storage — jako úložiště pro nová nahrání.",
|
||||
"Connection Closed Without Response": "Připojení uzavřeno bez odezvy",
|
||||
"Connection Timed Out": "Připojení vypršelo",
|
||||
"Continue": "Pokračovat",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Deaktivujte svůj účet nyní; po :days dnech bude trvale vymazán",
|
||||
"Deactivated the account \":subject\"": "Deaktivoval(a) účet \":subject\"",
|
||||
"Default": "Výchozí",
|
||||
"Defaults to the file name.": "Ve výchozím stavu název souboru.",
|
||||
"Delete": "Odstranit",
|
||||
"Delete :name": "Smazat :name",
|
||||
"Delete account": "Smazat účet",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Zamítnout",
|
||||
"Deny this request?": "Zamítnout tuto žádost?",
|
||||
"Description": "Popis",
|
||||
"Description (optional)": "Popis (nepovinné)",
|
||||
"Detach": "Odpojit",
|
||||
"Details": "Informace",
|
||||
"Disable": "Zakázat",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Přejít na stranu :page",
|
||||
"Go to the login form": "Přejít na přihlašovací formulář",
|
||||
"Gone": "Pryč",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Šedá",
|
||||
"Green": "Zelená",
|
||||
"Group": "Skupina",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Přihlášení",
|
||||
"Logout": "Odhlášení",
|
||||
"Loop Detected": "Zjištěna smyčka",
|
||||
"Mail provider settings saved.": "Nastavení poskytovatele e-mailu uloženo.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "údržbový mód",
|
||||
"Manage custom fields": "Správa vlastních polí",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Zatím s vámi nebyly sdíleny žádné soubory.",
|
||||
"No files here yet.": "Zatím zde nejsou žádné soubory.",
|
||||
"No files or folders match your search.": "Vašemu hledání neodpovídá žádný soubor ani složka.",
|
||||
"No files yet. Upload the first one.": "Zatím žádné soubory. Nahrajte první.",
|
||||
"No folder": "Žádná složka",
|
||||
"No groups match these filters.": "Těmto filtrům neodpovídá žádná skupina.",
|
||||
"No groups yet.": "Zatím žádné skupiny.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Zatím nebyl přidán žádný příjemce.",
|
||||
"No requests match your search.": "Vašemu hledání neodpovídá žádná žádost.",
|
||||
"No roles match this filter.": "Tomuto filtru neodpovídá žádná role.",
|
||||
"No service account key has been saved yet.": "Zatím nebyl uložen žádný klíč servisního účtu.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Bez omezení velikosti. Nahrávání lze pozastavit a po přerušení pokračuje automaticky.",
|
||||
"No users match these filters.": "Těmto filtrům neodpovídá žádný uživatel.",
|
||||
"No users yet.": "Zatím žádní uživatelé.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Oranžová",
|
||||
"Origin Is Unreachable": "Původ je nedosažitelný",
|
||||
"Outgoing email": "Odchozí e-mail",
|
||||
"Outgoing email theme": "Motiv odchozích e-mailů",
|
||||
"Page :current of :last": "Stránka :current z :last",
|
||||
"Page Expired": "Stránka vypršela",
|
||||
"Pagination Navigation": "Stránkování navigace",
|
||||
"Partial Content": "Částečný obsah",
|
||||
"Password": "Heslo",
|
||||
"Password reset": "Obnova hesla",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Vložte soubor s klíčem JSON servisního účtu s právem číst a zapisovat objekty v bucketu. Ukládá se zašifrovaný a už se nikdy nezobrazí.",
|
||||
"Path": "Cesta",
|
||||
"Payload Too Large": "Příliš velké užitečné zatížení",
|
||||
"Payment Required": "Platba vyžadována",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Nastavení veřejného seznamu",
|
||||
"Public listing URL": "URL veřejného seznamu",
|
||||
"Public pages & portal": "Veřejné stránky a portál",
|
||||
"Public pages and client portal theme": "Motiv veřejných stránek a klientského portálu",
|
||||
"Published": "Zveřejněno",
|
||||
"Purple": "Fialová",
|
||||
"Quota": "Kvóta",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Obnovit",
|
||||
"Restore :name": "Obnovit :name",
|
||||
"Restricted extension": "Omezená přípona",
|
||||
"Result": "Výsledek",
|
||||
"results": "výsledky",
|
||||
"Review membership requests": "Posuzování žádostí o členství",
|
||||
"Review pending requests": "Posoudit čekající žádosti",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Název role",
|
||||
"Role updated.": "Role aktualizována.",
|
||||
"Roles": "Role",
|
||||
"S3-compatible": "Kompatibilní s S3",
|
||||
"Sample email": "Ukázkový e-mail",
|
||||
"Save": "Uložit",
|
||||
"Save :name": "Uložit :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Odešle hned teď skutečný e-mail mimo frontu, abyste si ověřili, že nastavení výše opravdu funguje.",
|
||||
"Separate paragraphs with a blank line.": "Odstavce oddělujte prázdným řádkem.",
|
||||
"Server Error": "Chyba serveru",
|
||||
"Service account key": "Klíč servisního účtu",
|
||||
"Service Unavailable": "Služba je nedostupná",
|
||||
"Session Has Expired": "Platnost relace vypršela",
|
||||
"Set file expiration dates": "Nastavování data vypršení souborů",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Nastavení",
|
||||
"Settings that apply to the whole installation": "Nastavení platná pro celou instalaci",
|
||||
"Setup complete": "Nastavení dokončeno",
|
||||
"Share a file with the people who manage your account.": "Sdílejte soubor s lidmi, kteří spravují váš účet.",
|
||||
"Share files with several clients at once": "Sdílejte soubory s několika klienty najednou",
|
||||
"Shared the folder \":subject\" with :target": "Nasdílel(a) složku \":subject\" s :target",
|
||||
"Shared with": "Sdíleno s",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Přepínací protokoly",
|
||||
"System": "Systém",
|
||||
"System Administrator": "Správce systému",
|
||||
"System settings": "Nastavení systému",
|
||||
"System settings were updated": "Nastavení systému bylo aktualizováno",
|
||||
"System users": "Uživatelé systému",
|
||||
"Tag": "Štítek",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Test",
|
||||
"Text": "Text",
|
||||
"Text or author": "Text nebo autor",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Tohle nevypadá jako soubor s klíčem servisního účtu: není to platný JSON.",
|
||||
"That service account key file is missing its :field.": "V tomto souboru s klíčem servisního účtu chybí :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Účet :name bude trvale smazán. Tuto akci nelze vrátit zpět.",
|
||||
"The account password was changed": "Heslo k účtu bylo změněno",
|
||||
"The account request of :name will be denied and the account deleted.": "Žádost o účet od :name bude zamítnuta a účet smazán.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Skupiny, do kterých patříte, a ty, o vstup do kterých můžete požádat",
|
||||
"The header/footer style used for every outgoing notification email.": "Styl záhlaví a zápatí použitý u každého odchozího e-mailu s upozorněním.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Vzhled veřejných stránek pro návštěvníky, klientského portálu a odchozích e-mailů s upozorněními.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Samotný poštovní server se nastavuje v souboru .env této instalace (proměnné MAIL_*), nikoli zde.",
|
||||
"The people who administer this installation and upload files": "Lidé, kteří spravují tuto instalaci a nahrávají soubory",
|
||||
"The people you share files with": "Lidé, se kterými sdílíte soubory",
|
||||
"The provided two-factor authentication code was invalid.": "Zadaný kód dvoufázového ověření nebyl platný.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Zatím není co nastavovat.",
|
||||
"Test email from :site": "Zkušební e-mail ze :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Tyto soubory budou trvale odstraněny z disku. Tuto akci nelze vrátit zpět.",
|
||||
"Theme": "Motiv",
|
||||
"Theme updated.": "Motiv aktualizován.",
|
||||
"Theming": "Motivy",
|
||||
"Theming settings": "Nastavení motivů",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Toto nahrání by překročilo vaši kvótu úložiště :quota MB.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Tip: soubory a složky přesunete přetažením na složku nebo na drobečkovou navigaci.",
|
||||
"Title": "Nadpis",
|
||||
"Title (optional)": "Nadpis (nepovinné)",
|
||||
"TLS": "TLS",
|
||||
"to": "na",
|
||||
"To": "Komu",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Vybráno příliš mnoho souborů. Zvolte menší výběr a zkuste to znovu.",
|
||||
"Too Many Requests": "Příliš mnoho požadavků",
|
||||
"Transactional email settings for this installation": "Nastavení transakčních e-mailů této instalace",
|
||||
"Transfers — last 30 days": "Přenosy — posledních 30 dní",
|
||||
"Translate": "přeložit",
|
||||
"Translate It": "Přelož to",
|
||||
"Turns all transactional email for this installation on or off.": "Zapíná nebo vypíná všechny transakční e-maily této instalace.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Nahrál(a) soubor \":subject\"",
|
||||
"Uploader": "Nahrál(a)",
|
||||
"Uploads": "Nahrané soubory",
|
||||
"Uploads (chart)": "Nahrané soubory (graf)",
|
||||
"URI Too Long": "URI je příliš dlouhý",
|
||||
"URL slug": "URL identifikátor",
|
||||
"Use a custom link instead of a random one": "Použít vlastní odkaz místo náhodného",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Zobrazuje se jen tentokrát. Ukládáme pouze otisk, takže jej později nelze obnovit.",
|
||||
"Copy to clipboard": "Kopírovat do schránky",
|
||||
"Copied": "Zkopírováno",
|
||||
"Your tokens": "Vaše tokeny",
|
||||
"Revoking a token takes effect immediately.": "Zneplatnění tokenu se projeví okamžitě.",
|
||||
"You have not created any tokens yet.": "Zatím jste nevytvořili žádné tokeny.",
|
||||
"Expired": "Vypršel",
|
||||
"Never used": "Nikdy nepoužit",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Správa vlastních polí klientů",
|
||||
"Moderate comments": "Moderování komentářů",
|
||||
"Only me": "Jen já",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Viditelné pro každého, kdo může tento soubor otevřít, včetně nepřihlášených návštěvníků.",
|
||||
"Anyone, including visitors who are not logged in": "Kdokoli, včetně nepřihlášených návštěvníků",
|
||||
"Staff and clients": "Tým a klienti",
|
||||
"Staff only": "Jen tým",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Již napsané komentáře zůstávají čitelné, i když soubor přestane přijímat nové.",
|
||||
"Comments awaiting approval": "Komentáře čekající na schválení",
|
||||
"Comment settings": "Nastavení komentářů",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Komentáře od nepřihlášených návštěvníků. Nikdo je neuvidí, dokud je neschválíte.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Připojte externí bucket kompatibilní s S3 (AWS S3, MinIO nebo jinou službu kompatibilní s S3) jako úložiště pro nově nahrávané soubory.",
|
||||
"Custom endpoint": "Vlastní koncový bod",
|
||||
"Custom field created.": "Vlastní pole vytvořeno.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Žádné neúspěšné úlohy.",
|
||||
"No files match these filters.": "Těmto filtrům neodpovídá žádný soubor.",
|
||||
"None of the selected files could be edited.": "Žádný z vybraných souborů nešlo upravit.",
|
||||
"Nothing is waiting for approval.": "Nic nečeká na schválení.",
|
||||
"of :count": "z :count",
|
||||
"of :quota": "z :quota",
|
||||
"on": "dne",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Stránka",
|
||||
"Path prefix": "Předpona cesty",
|
||||
"Pending": "Čeká",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Pravidelně kontroluje veřejný repozitář projectsend.org, zda nevyšla novější verze, a zobrazí upozornění v přehledu. Nikdy se nic nestahuje ani nenasazuje automaticky.",
|
||||
"Permanently failed": "Trvale neúspěšné",
|
||||
"Post comment": "Odeslat komentář",
|
||||
"Preview of :name": "Náhled :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Odstranit dvoufaktorové ověření",
|
||||
"Remove two-factor authentication?": "Odstranit dvoufaktorové ověření?",
|
||||
"Require a verified email address": "Vyžadovat ověřenou e-mailovou adresu",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Spusťte je jako uživatel, pod kterým běží váš webový server. Celý postup najdete v INSTALL.md.",
|
||||
"Score threshold": "Práh skóre",
|
||||
"Search timezones…": "Hledat časová pásma…",
|
||||
"Security check failed. Please try again.": "Bezpečnostní kontrola selhala. Zkuste to prosím znovu.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Tento web je chráněn službou reCAPTCHA a platí Zásady ochrany soukromí a Smluvní podmínky společnosti Google.",
|
||||
"Those files have reached their download limit.": "Tyto soubory dosáhly svého limitu stažení.",
|
||||
"Timezone": "Časové pásmo",
|
||||
"To update, back up your database and files, then:": "Chcete-li aktualizovat, zazálohujte databázi a soubory a poté:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Chcete-li aktualizovat, postupujte podle kroků v INSTALL.md — nejprve zálohujte, pak rozbalte vydání a spusťte migrace.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Vypněte to jen tehdy, pokud tento server nemá provádět žádné odchozí požadavky. Pokud službu nelze kontaktovat, heslo se stejně přijme, takže offline instalace tím nikdy není blokována.",
|
||||
"Two-factor authentication removed.": "Dvoufaktorové ověření odstraněno.",
|
||||
"Two-factor authentication removed by an admin": "Dvoufaktorové ověření odstraněné administrátorem",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Váš účet je nastaven na :timezone, takže se tím nezmění, jak data vypadají pro vás.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Váš vlastní tenant, ne „common“. Microsoft umožňuje uživateli změnit e-mailovou adresu účtu, takže přihlášení je důvěryhodné jen tehdy, když token přišel od tenanta, kterého jste zde uvedli.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Váš poskytovatel odmítl tajný klíč této instalace, takže chráněné formuláře procházejí bez kontroly. Zkontrolujte tajný klíč níže.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Vaše veřejné formuláře jsou chráněny. Kdykoli můžete přejít na vlastní klíče."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Vaše veřejné formuláře jsou chráněny. Kdykoli můžete přejít na vlastní klíče.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Pokud jste se k starší verzi vrátili záměrně, spusťte php artisan projectsend:update, aby databáze odpovídala obnovenému kódu.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Než začne hledat vydání, než ho stáhne a než sáhne na vaši instalaci, vždy se zeptá — a ověří kontrolní součet toho, co stáhne. Celý postup najdete v UPDATE.md.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Nové soubory jsou na místě, ale databáze byla povýšena jen na :applied. Dokud aktualizace neproběhne, běží tato instalace na kódu, kterému její databáze neodpovídá.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Pravidelně kontroluje veřejný repozitář projectsend.org, zda nevyšla novější verze, a zobrazí upozornění v přehledu. Nikdy se nic nestahuje ani nenasazuje automaticky — aktualizaci vždy spouštíte vy a aktualizační skript se před každým krokem zeptá.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Databáze byla aktualizována na :applied, ale PHP stále obsluhuje kód, který měl zkompilovaný předtím. Po jeho znovunačtení toto upozornění zmizí.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Databáze byla aktualizována na :applied, ale tento kontejner stále běží na kódu, se kterým byl spuštěn.",
|
||||
"This server is running version :running, but version :applied is installed": "Tento server běží na verzi :running, ale nainstalovaná je verze :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Chcete-li aktualizovat, spusťte v adresáři instalace sudo ./update.sh — než něco stáhne nebo změní, vždy se zeptá.",
|
||||
"To update, run this in the install directory:": "Chcete-li aktualizovat, spusťte v adresáři instalace toto:",
|
||||
"Version :running is installed, but the update has not been run": "Verze :running je nainstalovaná, ale aktualizace nebyla spuštěna",
|
||||
"Updated ProjectSend to :to, from :from": "Aktualizoval(a) ProjectSend na :to z verze :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend byl aktualizován na novou verzi",
|
||||
"Come and say hello": "Přijďte se pozdravit",
|
||||
"Continue to the dashboard": "Pokračovat na přehled",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Připojit se na Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend má Discord: novinky o vydáních, pomoc, když se něco chová divně, a další lidé, kteří provozují stejný software. Jsme tam taky.",
|
||||
"What's new": "Novinky",
|
||||
"What each release brought to this installation.": "Co jednotlivá vydání přinesla této instalaci.",
|
||||
"Add the rest of your team": "Přidejte zbytek týmu",
|
||||
"Add your first client": "Přidejte prvního klienta",
|
||||
"Check the scheduler is running": "Zkontrolujte, že plánovač běží",
|
||||
"Choose how your email looks": "Vyberte, jak vypadají vaše e-maily",
|
||||
"Choose how your file lists look": "Vyberte, jak vypadají seznamy souborů",
|
||||
"Drop one in and choose who gets it.": "Vložte jeden a vyberte, kdo ho dostane.",
|
||||
"Essential": "Nezbytné",
|
||||
"Expiring files and queued email need it.": "Vypršené soubory a e-maily ve frontě to potřebují.",
|
||||
"Four layouts for the client-facing pages.": "Čtyři rozvržení stránek, které vidí klienti.",
|
||||
"Getting started": "První kroky",
|
||||
"Group your clients": "Seskupte své klienty",
|
||||
"Made with care in Argentina. Shared with the world.": "Vyrobeno s péčí v Argentině. Sdíleno se světem.",
|
||||
"Previewed on a real message.": "Náhled na skutečné zprávě.",
|
||||
"Resets and share links go out through it.": "Obnovení hesel a odkazy ke sdílení jdou přes něj.",
|
||||
"Set up your mail server": "Nastavte svůj poštovní server",
|
||||
"Share once instead of six times.": "Sdílejte jednou místo šestkrát.",
|
||||
"Staff accounts, each scoped by its role.": "Účty týmu, každý omezený svou rolí.",
|
||||
"Thank you for installing ProjectSend": "Děkujeme za instalaci ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Děkujeme za aktualizaci ProjectSend",
|
||||
"The people you send files to.": "Lidé, kterým posíláte soubory.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Krátká verze toho, co udělat na nové instalaci. Nic z toho nevyprší.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Nyní máte :version a vše je zase v provozu. Děkujeme, že své soubory nadále svěřujete ProjectSendu.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Nyní máte :version, z :previous, a vše je zase v provozu. Děkujeme, že své soubory nadále svěřujete ProjectSendu.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Používáte verzi :version a je vaše. Tohle se vyplatí udělat jako první.",
|
||||
"Thank you for choosing ProjectSend": "Děkujeme, že jste si vybrali ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Váš web je připravený a jeho provoz je na nás. Tohle se vyplatí udělat jako první.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 zachová vše. Nepřečtená upozornění se nikdy nemažou, ať jsou jakkoli stará.",
|
||||
":version is available": "Je k dispozici verze :version",
|
||||
":version on :date": ":version dne :date",
|
||||
"Check now": "Zkontrolovat nyní",
|
||||
"Checked a moment ago, so this is the answer from then.": "Kontrola proběhla před chvílí, takže tohle je odpověď z té doby.",
|
||||
"Checking…": "Kontroluje se…",
|
||||
"Could not reach the update feed.": "Nepodařilo se spojit s kanálem aktualizací.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Před jakýmkoli zásahem vytvoř zálohu databáze a zastav se, pokud záloha selže.",
|
||||
"Housekeeping": "Úklid",
|
||||
"Keep failed jobs for": "Uchovávat neúspěšné úlohy po dobu",
|
||||
"Keep read notifications for": "Uchovávat přečtená upozornění po dobu",
|
||||
"Last checked :date": "Poslední kontrola: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Nechat PHP a worker být, pro případ, že je restartuješ sám.",
|
||||
"Other options": "Další možnosti",
|
||||
"Report which version is installed and which is available, and change nothing.": "Oznámí, která verze je nainstalovaná a která je k dispozici, a nic nezmění.",
|
||||
"Retention updated.": "Doba uchování aktualizována.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Spuštěný bez voleb se skript na každou z těchto věcí ptá cestou. Tyhle jsou pro běhy, kde odpověď už znáš.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Přeskočí otázku na zálohu, pro běh, u kterého nesedíš.",
|
||||
"The latest release (:tag) is not a version this can compare.": "Nejnovější vydání (:tag) není verze, kterou lze zde porovnat.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Dvě věci se hromadí samy. Tohle jsou lhůty, kterými se řídí denní úklidy výše.",
|
||||
"Up to date": "Aktuální",
|
||||
"Update checks are not available on this edition.": "Kontrola aktualizací není v této edici k dispozici.",
|
||||
"Update options": "Možnosti aktualizace",
|
||||
"Updated": "Aktualizováno",
|
||||
"Version :version is available. You are running :current.": "Je k dispozici verze :version. Používáš :current.",
|
||||
"You are up to date, running version :current.": "Máš aktuální verzi :current.",
|
||||
"days": "dní",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help vypíše všechny volby a UPDATE.md provede celým postupem, včetně ručního.",
|
||||
"Fetch dashboard news": "Načíst novinky na nástěnku",
|
||||
"Purge API request logs": "Vyčistit protokol požadavků API",
|
||||
"Purge erased accounts": "Vyčistit smazané účty",
|
||||
"Purge expired files": "Vyčistit soubory s vypršelou platností",
|
||||
"Purge failed jobs": "Vyčistit neúspěšné úlohy",
|
||||
"Purge orphan files": "Vyčistit osiřelé soubory",
|
||||
"Purge read notifications": "Vyčistit přečtená upozornění",
|
||||
"Purge stale chunked uploads": "Vyčistit opuštěná dílčí nahrávání",
|
||||
"Purge zip downloads": "Vyčistit stažené ZIP archivy",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Odpovídá tomuto filtru: :count, od nejnovějších",
|
||||
"A file was previewed via the public group listing": "Soubor byl zobrazen v náhledu přes veřejný seznam skupin",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Kdokoli s odkazem může otevřít obrázek, video, zvukový soubor nebo PDF přímo v prohlížeči místo stahování. Vypněte to, pokud má být stažení jediný způsob, jak veřejný soubor zobrazit.",
|
||||
"Clients can preview files": "Klienti mohou zobrazovat náhledy souborů",
|
||||
"Downloads & previews": "Stažení a náhledy",
|
||||
"Downloads: :count": "Stažení: :count",
|
||||
"Let visitors preview public files": "Umožnit návštěvníkům zobrazit náhled veřejných souborů",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Umožní klientům otevřít obrázek, video, zvukový soubor nebo PDF přímo v portálu, ne si ho jen stáhnout. Vypněte to, pokud má být stažení jediný způsob, jak soubor zobrazit. Tým může náhled zobrazit vždy.",
|
||||
"No downloads match these filters.": "Těmto filtrům neodpovídá žádné stažení.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Tento soubor si zatím nikdo nestáhl ani nezobrazil v náhledu.",
|
||||
"Preview :name": "Náhled souboru :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Zobrazil(a) náhled souboru \":subject\" přes veřejný seznam skupin",
|
||||
"Previews: :count": "Náhledy: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Tato instalace si obrazy sestavuje sama, takže stažení hotového obrazu nic nezmění. Pokud vydání změnilo composer.lock nebo frontend, spusťte také composer install a npm run build — celý postup najdete v UPDATE.md.",
|
||||
"To update, run this in the directory you cloned:": "Aktualizaci provedete tímto příkazem v adresáři, který jste naklonovali:",
|
||||
"View all previews (:count)": "Zobrazit všechny náhledy (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Váš prohlížeč tento soubor nepřehraje. Stáhněte si ho a zobrazte si ho tak."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count Downloads",
|
||||
":count file deleted.|:count files deleted.": ":count Datei gelöscht.|:count Dateien gelöscht.",
|
||||
":count file imported.|:count files imported.": ":count Datei importiert.|:count Dateien importiert.",
|
||||
":count files": ":count Dateien",
|
||||
":count folders": ":count Ordner",
|
||||
":count items have been shared with you": ":count Elemente wurden mit Ihnen geteilt",
|
||||
":count members": ":count Mitglieder",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(und :count weiterer Fehler)",
|
||||
"(and :count more errors)": "(und :count weiterer Fehler)|(und :count weitere Fehler)|(und :count weitere Fehler)",
|
||||
"(deleted account)": "(gelöschtes Konto)",
|
||||
"0 means unlimited.": "0 bedeutet unbegrenzt.",
|
||||
"A client account request was approved": "Eine Kontoanfrage eines Kunden wurde genehmigt",
|
||||
"A client account request was denied": "Eine Kontoanfrage eines Kunden wurde abgelehnt",
|
||||
"A client left a group": "Ein Kunde hat eine Gruppe verlassen",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Ein neues Kundenkonto wurde erstellt: :name (:email).",
|
||||
"A new client has registered": "Ein neuer Kunde hat sich registriert",
|
||||
"A new ProjectSend version is available": "Eine neue ProjectSend-Version ist verfügbar",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Die Seite einer öffentlichen Gruppe ist immer über ihren eigenen Link erreichbar, sobald Sie sie als öffentlich markieren — dieser Abschnitt steuert nur die Basis-URL und ein optionales Verzeichnis, das alle öffentlichen Gruppen und Dateien auf einmal auflistet.",
|
||||
"A public link was created": "Ein öffentlicher Link wurde erstellt",
|
||||
"A public link was revoked": "Ein öffentlicher Link wurde widerrufen",
|
||||
"A role was created": "Eine Rolle wurde erstellt",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Administrator: Gruppenmitgliedschaft angefragt",
|
||||
"Admin: new client registered": "Administrator: neuer Kunde registriert",
|
||||
"Administration": "Verwaltung",
|
||||
"After uploading you can assign it to clients and groups": "Nach dem Hochladen können Sie sie Kunden und Gruppen zuweisen",
|
||||
"Agree": "Zustimmen",
|
||||
"All": "Alle",
|
||||
"All actions": "Alle Aktionen",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Alle Kommentare",
|
||||
"All failed jobs deleted.": "Alle fehlgeschlagenen Aufträge wurden gelöscht.",
|
||||
"All files": "Alle Dateien",
|
||||
"All groups": "Alle Gruppen",
|
||||
"All permissions": "Alle Berechtigungen",
|
||||
"All rights reserved.": "Alle Rechte vorbehalten.",
|
||||
"All roles": "Alle Rollen",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Wer ihn verwendet, kann die Datei nicht mehr herunterladen.",
|
||||
"Appearance": "Darstellung",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Gilt für die öffentlichen Gruppen- und Dateiseiten für Gäste sowie für das Kundenportal \"Meine Dateien\".",
|
||||
"Apply filters": "Filter anwenden",
|
||||
"Approve": "Genehmigen",
|
||||
"Approve account requests": "Kontoanfragen genehmigen",
|
||||
"Approve group membership requests": "Anfragen auf Gruppenmitgliedschaft genehmigen",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Möchten Sie Ihr Konto wirklich löschen?",
|
||||
"Assign": "Zuordnen",
|
||||
"Assign categories to files": "Kategorien zu Dateien zuweisen",
|
||||
"Assigned": "Zugewiesen",
|
||||
"Assigned clients": "Zugewiesene Kunden",
|
||||
"Assigned the file \":subject\" to :target": "Hat die Datei \":subject\" an :target zugewiesen",
|
||||
"Associate": "Assoziieren",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Autor",
|
||||
"Available groups": "Verfügbare Gruppen",
|
||||
"Available placeholders": "Verfügbare Platzhalter",
|
||||
"Back to My files": "Zurück zu Meine Dateien",
|
||||
"Bad Gateway": "Fehlerhaftes Gateway",
|
||||
"Bad Request": "Ungültige Anfrage",
|
||||
"Bandwidth Limit Exceeded": "Bandbreitenlimit überschritten",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Inhalt",
|
||||
"Browse": "Durchsuchen",
|
||||
"Browse publicly shared groups and files below.": "Durchsuchen Sie unten die öffentlich geteilten Gruppen und Dateien.",
|
||||
"Built-in": "Integriert",
|
||||
"Built-in role names cannot be changed.": "Namen integrierter Rollen können nicht geändert werden.",
|
||||
"Built-in roles cannot be deleted.": "Integrierte Rollen können nicht gelöscht werden.",
|
||||
"Call to action": "Handlungsaufforderung",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Passwort bestätigen",
|
||||
"Conflict": "Konflikt",
|
||||
"Connect": "Verbinden",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Verbinden Sie einen externen Bucket — S3-kompatibel (AWS S3, MinIO, Backblaze) oder Google Cloud Storage — als Speicher für neue Uploads.",
|
||||
"Connection Closed Without Response": "Verbindung ohne Antwort getrennt",
|
||||
"Connection Timed Out": "Verbindungszeit überschritten",
|
||||
"Continue": "Weiter",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Deaktivieren Sie Ihr Konto jetzt; nach :days Tagen wird es endgültig gelöscht",
|
||||
"Deactivated the account \":subject\"": "Hat das Konto \":subject\" deaktiviert",
|
||||
"Default": "Standard",
|
||||
"Defaults to the file name.": "Standardmäßig der Dateiname.",
|
||||
"Delete": "Löschen",
|
||||
"Delete :name": ":name löschen",
|
||||
"Delete account": "Konto löschen",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Ablehnen",
|
||||
"Deny this request?": "Diese Anfrage ablehnen?",
|
||||
"Description": "Beschreibung",
|
||||
"Description (optional)": "Beschreibung (optional)",
|
||||
"Detach": "Trennen",
|
||||
"Details": "Details",
|
||||
"Disable": "Deaktivieren",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Gehe zur Seite :page",
|
||||
"Go to the login form": "Zum Anmeldeformular",
|
||||
"Gone": "Nicht mehr verfügbar",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Grau",
|
||||
"Green": "Grün",
|
||||
"Group": "Gruppe",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"Loop Detected": "Endlosschleife erkannt",
|
||||
"Mail provider settings saved.": "Einstellungen des E-Mail-Anbieters gespeichert.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Wartungsmodus",
|
||||
"Manage custom fields": "Benutzerdefinierte Felder verwalten",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Es wurden noch keine Dateien mit Ihnen geteilt.",
|
||||
"No files here yet.": "Hier gibt es noch keine Dateien.",
|
||||
"No files or folders match your search.": "Keine Datei und kein Ordner entspricht Ihrer Suche.",
|
||||
"No files yet. Upload the first one.": "Noch keine Dateien. Laden Sie die erste hoch.",
|
||||
"No folder": "Kein Ordner",
|
||||
"No groups match these filters.": "Keine Gruppe entspricht diesen Filtern.",
|
||||
"No groups yet.": "Noch keine Gruppen.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Noch keine Empfänger hinzugefügt.",
|
||||
"No requests match your search.": "Keine Anfrage entspricht Ihrer Suche.",
|
||||
"No roles match this filter.": "Keine Rolle entspricht diesem Filter.",
|
||||
"No service account key has been saved yet.": "Es wurde noch kein Dienstkontoschlüssel gespeichert.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Keine Größenbeschränkung. Uploads können pausiert werden und laufen nach Unterbrechungen automatisch weiter.",
|
||||
"No users match these filters.": "Kein Benutzer entspricht diesen Filtern.",
|
||||
"No users yet.": "Noch keine Benutzer.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Orange",
|
||||
"Origin Is Unreachable": "Quelle ist nicht erreichbar",
|
||||
"Outgoing email": "Ausgehende E-Mail",
|
||||
"Outgoing email theme": "Design für ausgehende E-Mails",
|
||||
"Page :current of :last": "Seite :current von :last",
|
||||
"Page Expired": "Seite abgelaufen",
|
||||
"Pagination Navigation": "Seiten-Navigation",
|
||||
"Partial Content": "Teilinhalt",
|
||||
"Password": "Passwort",
|
||||
"Password reset": "Passwort zurücksetzen",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Fügen Sie die JSON-Schlüsseldatei eines Dienstkontos mit Lese- und Schreibzugriff auf die Objekte des Buckets ein. Sie wird verschlüsselt gespeichert und nie wieder angezeigt.",
|
||||
"Path": "Pfad",
|
||||
"Payload Too Large": "Nutzlast zu groß",
|
||||
"Payment Required": "Zahlung erforderlich",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Einstellungen für das öffentliche Verzeichnis",
|
||||
"Public listing URL": "URL des öffentlichen Verzeichnisses",
|
||||
"Public pages & portal": "Öffentliche Seiten & Portal",
|
||||
"Public pages and client portal theme": "Design für öffentliche Seiten und Kundenportal",
|
||||
"Published": "Veröffentlicht",
|
||||
"Purple": "Violett",
|
||||
"Quota": "Kontingent",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Wiederherstellen",
|
||||
"Restore :name": ":name wiederherstellen",
|
||||
"Restricted extension": "Eingeschränkte Dateiendung",
|
||||
"Result": "Ergebnis",
|
||||
"results": "Ergebnissen",
|
||||
"Review membership requests": "Mitgliedschaftsanfragen prüfen",
|
||||
"Review pending requests": "Offene Anfragen prüfen",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Rollenname",
|
||||
"Role updated.": "Rolle aktualisiert.",
|
||||
"Roles": "Rollen",
|
||||
"S3-compatible": "S3-kompatibel",
|
||||
"Sample email": "Beispiel-E-Mail",
|
||||
"Save": "Speichern",
|
||||
"Save :name": "Sparen Sie :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Sendet sofort eine echte E-Mail an der Warteschlange vorbei, damit Sie prüfen können, ob die Einstellungen oben tatsächlich funktionieren.",
|
||||
"Separate paragraphs with a blank line.": "Trennen Sie Absätze durch eine Leerzeile.",
|
||||
"Server Error": "Interner Fehler",
|
||||
"Service account key": "Dienstkontoschlüssel",
|
||||
"Service Unavailable": "Service nicht verfügbar",
|
||||
"Session Has Expired": "Sitzung ist abgelaufen",
|
||||
"Set file expiration dates": "Ablaufdaten für Dateien festlegen",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Einstellungen",
|
||||
"Settings that apply to the whole installation": "Einstellungen, die für die gesamte Installation gelten",
|
||||
"Setup complete": "Einrichtung abgeschlossen",
|
||||
"Share a file with the people who manage your account.": "Teilen Sie eine Datei mit den Personen, die Ihr Konto betreuen.",
|
||||
"Share files with several clients at once": "Dateien mit mehreren Kunden auf einmal teilen",
|
||||
"Shared the folder \":subject\" with :target": "Hat den Ordner \":subject\" mit :target geteilt",
|
||||
"Shared with": "Geteilt mit",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Protokollwechsel",
|
||||
"System": "System",
|
||||
"System Administrator": "Systemadministrator",
|
||||
"System settings": "Systemeinstellungen",
|
||||
"System settings were updated": "Systemeinstellungen wurden aktualisiert",
|
||||
"System users": "Systembenutzer",
|
||||
"Tag": "Stichwort",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Test",
|
||||
"Text": "Text",
|
||||
"Text or author": "Text oder Autor",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Das sieht nicht nach einer Dienstkonto-Schlüsseldatei aus: Es ist kein gültiges JSON.",
|
||||
"That service account key file is missing its :field.": "In dieser Dienstkonto-Schlüsseldatei fehlt :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Das Konto von :name wird endgültig gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||
"The account password was changed": "Das Kontopasswort wurde geändert",
|
||||
"The account request of :name will be denied and the account deleted.": "Die Kontoanfrage von :name wird abgelehnt und das Konto gelöscht.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Die Gruppen, denen Sie angehören, und die, deren Beitritt Sie anfragen können",
|
||||
"The header/footer style used for every outgoing notification email.": "Der Kopf- und Fußzeilenstil für jede ausgehende Benachrichtigungs-E-Mail.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Das Erscheinungsbild der öffentlichen Gästeseiten, des Kundenportals und der ausgehenden Benachrichtigungs-E-Mails.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Der Mailserver selbst wird in der .env-Datei dieser Installation konfiguriert (MAIL_*-Variablen), nicht hier.",
|
||||
"The people who administer this installation and upload files": "Die Personen, die diese Installation verwalten und Dateien hochladen",
|
||||
"The people you share files with": "Die Personen, mit denen Sie Dateien teilen",
|
||||
"The provided two-factor authentication code was invalid.": "Der eingegebene Zwei-Faktor-Authentifizierungscode war ungültig.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Es gibt noch nichts zu konfigurieren.",
|
||||
"Test email from :site": "Test-E-Mail von :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Diese Dateien werden endgültig von der Festplatte entfernt. Das lässt sich nicht rückgängig machen.",
|
||||
"Theme": "Design",
|
||||
"Theme updated.": "Design aktualisiert.",
|
||||
"Theming": "Design",
|
||||
"Theming settings": "Design-Einstellungen",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Dieser Upload würde Ihr Speicherkontingent von :quota MB überschreiten.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Tipp: Ziehen Sie Dateien und Ordner auf einen Ordner oder einen Pfadeintrag, um sie zu verschieben.",
|
||||
"Title": "Titel",
|
||||
"Title (optional)": "Titel (optional)",
|
||||
"TLS": "TLS",
|
||||
"to": "bis",
|
||||
"To": "An",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Zu viele Dateien ausgewählt. Wählen Sie weniger aus und versuchen Sie es erneut.",
|
||||
"Too Many Requests": "Zu viele Anfragen",
|
||||
"Transactional email settings for this installation": "Einstellungen für Transaktions-E-Mails dieser Installation",
|
||||
"Transfers — last 30 days": "Übertragungen — letzte 30 Tage",
|
||||
"Translate": "Übersetzen",
|
||||
"Translate It": "Übersetze es",
|
||||
"Turns all transactional email for this installation on or off.": "Schaltet sämtliche Transaktions-E-Mails dieser Installation ein oder aus.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Hat die Datei \":subject\" hochgeladen",
|
||||
"Uploader": "Hochgeladen von",
|
||||
"Uploads": "Uploads",
|
||||
"Uploads (chart)": "Uploads (Diagramm)",
|
||||
"URI Too Long": "URI ist zu lang",
|
||||
"URL slug": "URL-Slug",
|
||||
"Use a custom link instead of a random one": "Einen eigenen Link statt eines zufälligen verwenden",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Dies ist das einzige Mal, dass es angezeigt wird. Wir speichern nur einen Hash, es lässt sich später nicht wiederherstellen.",
|
||||
"Copy to clipboard": "In die Zwischenablage kopieren",
|
||||
"Copied": "Kopiert",
|
||||
"Your tokens": "Ihre Token",
|
||||
"Revoking a token takes effect immediately.": "Der Widerruf eines Tokens wirkt sofort.",
|
||||
"You have not created any tokens yet.": "Sie haben noch keine Token erstellt.",
|
||||
"Expired": "Abgelaufen",
|
||||
"Never used": "Nie verwendet",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Benutzerdefinierte Kundenfelder verwalten",
|
||||
"Moderate comments": "Kommentare moderieren",
|
||||
"Only me": "Nur ich",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Sichtbar für alle, die diese Datei öffnen können, auch für nicht angemeldete Besucher.",
|
||||
"Anyone, including visitors who are not logged in": "Alle, auch nicht angemeldete Besucher",
|
||||
"Staff and clients": "Team und Kunden",
|
||||
"Staff only": "Nur Team",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Bereits verfasste Kommentare bleiben lesbar, auch wenn eine Datei keine neuen mehr annimmt.",
|
||||
"Comments awaiting approval": "Kommentare, die auf Freigabe warten",
|
||||
"Comment settings": "Kommentareinstellungen",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Kommentare von nicht angemeldeten Besuchern. Niemand sieht sie, bevor Sie sie freigeben.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Verbinden Sie einen externen S3-kompatiblen Bucket (AWS S3, MinIO oder einen anderen S3-kompatiblen Dienst) als Speicher-Backend für neue Uploads.",
|
||||
"Custom endpoint": "Eigener Endpunkt",
|
||||
"Custom field created.": "Benutzerdefiniertes Feld erstellt.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Keine fehlgeschlagenen Aufträge.",
|
||||
"No files match these filters.": "Keine Datei entspricht diesen Filtern.",
|
||||
"None of the selected files could be edited.": "Keine der ausgewählten Dateien konnte bearbeitet werden.",
|
||||
"Nothing is waiting for approval.": "Es wartet nichts auf Freigabe.",
|
||||
"of :count": "von :count",
|
||||
"of :quota": "von :quota",
|
||||
"on": "am",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Seite",
|
||||
"Path prefix": "Pfadpräfix",
|
||||
"Pending": "Ausstehend",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Prüft regelmäßig das öffentliche Repository von projectsend.org auf eine neuere Version und zeigt einen Hinweis in der Übersicht. Es wird nie etwas automatisch heruntergeladen oder eingespielt.",
|
||||
"Permanently failed": "Endgültig fehlgeschlagen",
|
||||
"Post comment": "Kommentar absenden",
|
||||
"Preview of :name": "Vorschau von :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Zwei-Faktor-Authentifizierung entfernen",
|
||||
"Remove two-factor authentication?": "Zwei-Faktor-Authentifizierung entfernen?",
|
||||
"Require a verified email address": "Eine verifizierte E-Mail-Adresse verlangen",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Führe diese als der Benutzer aus, unter dem dein Webserver läuft. INSTALL.md enthält die vollständige Anleitung.",
|
||||
"Score threshold": "Score-Schwelle",
|
||||
"Search timezones…": "Zeitzonen suchen…",
|
||||
"Security check failed. Please try again.": "Die Sicherheitsprüfung ist fehlgeschlagen. Bitte versuche es erneut.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Diese Website ist durch reCAPTCHA geschützt, und es gelten die Datenschutzerklärung und die Nutzungsbedingungen von Google.",
|
||||
"Those files have reached their download limit.": "Diese Dateien haben ihr Download-Limit erreicht.",
|
||||
"Timezone": "Zeitzone",
|
||||
"To update, back up your database and files, then:": "Zum Aktualisieren sichere deine Datenbank und deine Dateien, dann:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Zum Aktualisieren folge den Schritten in INSTALL.md – zuerst sichern, dann die Version entpacken und die Migrationen ausführen.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Deaktiviere dies nur, wenn dieser Server keine ausgehenden Anfragen stellen soll. Ist der Dienst nicht erreichbar, wird das Passwort trotzdem akzeptiert, sodass eine Offline-Installation dadurch nie blockiert wird.",
|
||||
"Two-factor authentication removed.": "Zwei-Faktor-Authentifizierung entfernt.",
|
||||
"Two-factor authentication removed by an admin": "Zwei-Faktor-Authentifizierung von einer Administration entfernt",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Dein eigenes Konto ist auf :timezone eingestellt, daher ändert dies nicht, wie Datumsangaben für dich aussehen.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Dein eigener Mandant, nicht „common“. Microsoft erlaubt es Nutzern, die E-Mail-Adresse ihres Kontos zu ändern; eine Anmeldung ist daher nur vertrauenswürdig, wenn das Token von dem hier angegebenen Mandanten kam.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Dein Anbieter hat den geheimen Schlüssel dieser Installation abgelehnt, daher werden geschützte Formulare ungeprüft durchgelassen. Prüfe den geheimen Schlüssel unten.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Deine öffentlichen Formulare sind geschützt. Du kannst jederzeit zu deinen eigenen Schlüsseln wechseln."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Deine öffentlichen Formulare sind geschützt. Du kannst jederzeit zu deinen eigenen Schlüsseln wechseln.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Wenn du absichtlich zurückgegangen bist, führe php artisan projectsend:update aus, damit die Datenbank zu dem Code passt, den du wiederhergestellt hast.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Es fragt nach, bevor es nach einer Version sucht, bevor es eine herunterlädt und bevor es deine Installation anfasst – und prüft die Prüfsumme dessen, was es herunterlädt. UPDATE.md enthält die vollständige Anleitung.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Die neuen Dateien liegen bereit, aber die Datenbank ist nur bis :applied gebracht worden. Bis das Update läuft, arbeitet diese Installation mit Code, zu dem ihre Datenbank nicht passt.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Prüft regelmäßig das öffentliche Repository von projectsend.org auf eine neuere Version und zeigt einen Hinweis in der Übersicht. Es wird nie etwas automatisch heruntergeladen oder eingespielt – aktualisiert wird immer nur, wenn du es tust, und das Update-Skript fragt vor jedem Schritt nach.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Die Datenbank wurde auf :applied aktualisiert, und PHP liefert weiterhin den Code aus, den es vorher kompiliert hatte. Ein Neuladen lässt diesen Hinweis verschwinden.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Die Datenbank wurde auf :applied aktualisiert, und dieser Container läuft weiterhin mit dem Code, mit dem er gestartet ist.",
|
||||
"This server is running version :running, but version :applied is installed": "Dieser Server läuft mit Version :running, installiert ist aber Version :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Zum Aktualisieren führe sudo ./update.sh im Installationsverzeichnis aus – es fragt nach, bevor es etwas herunterlädt oder ändert.",
|
||||
"To update, run this in the install directory:": "Zum Aktualisieren führe das hier im Installationsverzeichnis aus:",
|
||||
"Version :running is installed, but the update has not been run": "Version :running ist installiert, aber das Update wurde nicht ausgeführt",
|
||||
"Updated ProjectSend to :to, from :from": "Hat ProjectSend auf :to aktualisiert, von :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend wurde auf eine neue Version aktualisiert",
|
||||
"Come and say hello": "Sagen Sie Hallo",
|
||||
"Continue to the dashboard": "Weiter zur Übersicht",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Discord beitreten",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend hat einen Discord: Neuigkeiten zu Releases, Hilfe, wenn sich etwas seltsam verhält, und andere Menschen, die dieselbe Software betreiben. Wir sind auch dort.",
|
||||
"What's new": "Neuerungen",
|
||||
"What each release brought to this installation.": "Was jede Version dieser Installation gebracht hat.",
|
||||
"Add the rest of your team": "Fügen Sie den Rest Ihres Teams hinzu",
|
||||
"Add your first client": "Fügen Sie Ihren ersten Kunden hinzu",
|
||||
"Check the scheduler is running": "Prüfen Sie, ob der Planer läuft",
|
||||
"Choose how your email looks": "Wählen Sie, wie Ihre E-Mails aussehen",
|
||||
"Choose how your file lists look": "Wählen Sie, wie Ihre Dateilisten aussehen",
|
||||
"Drop one in and choose who gets it.": "Legen Sie eine ab und wählen Sie, wer sie bekommt.",
|
||||
"Essential": "Unverzichtbar",
|
||||
"Expiring files and queued email need it.": "Ablaufende Dateien und wartende E-Mails hängen davon ab.",
|
||||
"Four layouts for the client-facing pages.": "Vier Layouts für die Seiten, die Kunden sehen.",
|
||||
"Getting started": "Erste Schritte",
|
||||
"Group your clients": "Fassen Sie Ihre Kunden zu Gruppen zusammen",
|
||||
"Made with care in Argentina. Shared with the world.": "Mit Sorgfalt in Argentinien gemacht. Mit der Welt geteilt.",
|
||||
"Previewed on a real message.": "Vorschau an einer echten Nachricht.",
|
||||
"Resets and share links go out through it.": "Passwort-Zurücksetzungen und Freigabelinks gehen darüber hinaus.",
|
||||
"Set up your mail server": "Richten Sie Ihren Mailserver ein",
|
||||
"Share once instead of six times.": "Einmal teilen statt sechsmal.",
|
||||
"Staff accounts, each scoped by its role.": "Teamkonten, jedes durch seine Rolle begrenzt.",
|
||||
"Thank you for installing ProjectSend": "Danke, dass Sie ProjectSend installiert haben",
|
||||
"Thank you for updating ProjectSend": "Danke, dass Sie ProjectSend aktualisiert haben",
|
||||
"The people you send files to.": "Die Menschen, denen Sie Dateien schicken.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Die Kurzfassung dessen, was auf einer neuen Installation zu tun ist. Nichts davon läuft ab.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Sie sind jetzt auf :version, und alles läuft wieder. Danke, dass Sie ProjectSend weiterhin Ihre Dateien anvertrauen.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Sie sind jetzt auf :version, zuvor :previous, und alles läuft wieder. Danke, dass Sie ProjectSend weiterhin Ihre Dateien anvertrauen.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Sie nutzen Version :version, und sie gehört Ihnen. Das hier lohnt sich zuerst.",
|
||||
"Thank you for choosing ProjectSend": "Danke, dass Sie sich für ProjectSend entschieden haben",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Ihre Website ist bereit, und der Betrieb ist unsere Sache. Das hier lohnt sich zuerst.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 behält alles. Ungelesene Benachrichtigungen werden nie gelöscht, egal wie alt sie sind.",
|
||||
":version is available": ":version ist verfügbar",
|
||||
":version on :date": ":version am :date",
|
||||
"Check now": "Jetzt prüfen",
|
||||
"Checked a moment ago, so this is the answer from then.": "Die Prüfung liegt einen Moment zurück, das ist also die Antwort von damals.",
|
||||
"Checking…": "Wird geprüft…",
|
||||
"Could not reach the update feed.": "Der Update-Feed war nicht erreichbar.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Vor jedem Eingriff einen Datenbank-Dump erstellen und abbrechen, wenn der Dump fehlschlägt.",
|
||||
"Housekeeping": "Aufräumen",
|
||||
"Keep failed jobs for": "Fehlgeschlagene Aufträge aufbewahren für",
|
||||
"Keep read notifications for": "Gelesene Benachrichtigungen aufbewahren für",
|
||||
"Last checked :date": "Zuletzt geprüft: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "PHP und den Worker unangetastet lassen, für den Fall, dass du sie selbst neu startest.",
|
||||
"Other options": "Weitere Optionen",
|
||||
"Report which version is installed and which is available, and change nothing.": "Meldet, welche Version installiert und welche verfügbar ist, und ändert nichts.",
|
||||
"Retention updated.": "Aufbewahrung aktualisiert.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Ohne Optionen ausgeführt, fragt das Skript unterwegs nach jeder dieser Angaben. Diese hier sind für Läufe, bei denen du die Antwort schon kennst.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Überspringt die Frage nach der Sicherung, für einen Lauf, bei dem du nicht davor sitzt.",
|
||||
"The latest release (:tag) is not a version this can compare.": "Die neueste Veröffentlichung (:tag) ist keine Version, die sich hier vergleichen lässt.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Zwei Dinge sammeln sich von allein an. Das sind die Zeiträume, nach denen sich die täglichen Bereinigungen oben richten.",
|
||||
"Up to date": "Aktuell",
|
||||
"Update checks are not available on this edition.": "Update-Prüfungen sind in dieser Edition nicht verfügbar.",
|
||||
"Update options": "Update-Optionen",
|
||||
"Updated": "Aktualisiert",
|
||||
"Version :version is available. You are running :current.": "Version :version ist verfügbar. Du verwendest :current.",
|
||||
"You are up to date, running version :current.": "Du bist auf dem neuesten Stand, mit Version :current.",
|
||||
"days": "Tage",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help listet jede Option auf, und UPDATE.md führt durch das gesamte Verfahren, auch von Hand.",
|
||||
"Fetch dashboard news": "Neuigkeiten fürs Dashboard abrufen",
|
||||
"Purge API request logs": "API-Anfrageprotokoll bereinigen",
|
||||
"Purge erased accounts": "Gelöschte Konten bereinigen",
|
||||
"Purge expired files": "Abgelaufene Dateien bereinigen",
|
||||
"Purge failed jobs": "Fehlgeschlagene Aufträge bereinigen",
|
||||
"Purge orphan files": "Verwaiste Dateien bereinigen",
|
||||
"Purge read notifications": "Gelesene Benachrichtigungen bereinigen",
|
||||
"Purge stale chunked uploads": "Abgebrochene Teil-Uploads bereinigen",
|
||||
"Purge zip downloads": "ZIP-Downloads bereinigen",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": ":count Treffer für diesen Filter, neueste zuerst",
|
||||
"A file was previewed via the public group listing": "Eine Datei wurde über das öffentliche Gruppenverzeichnis in der Vorschau geöffnet",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Wer den Link hat, kann ein Bild, ein Video, eine Audiodatei oder ein PDF im Browser öffnen, statt es herunterzuladen. Schalte es aus, damit sich eine öffentliche Datei nur per Download ansehen lässt.",
|
||||
"Clients can preview files": "Kunden können Dateien in der Vorschau öffnen",
|
||||
"Downloads & previews": "Downloads & Vorschauen",
|
||||
"Downloads: :count": "Downloads: :count",
|
||||
"Let visitors preview public files": "Besucher dürfen öffentliche Dateien in der Vorschau öffnen",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Lässt Kunden ein Bild, ein Video, eine Audiodatei oder ein PDF im Portal öffnen, statt es nur herunterzuladen. Schalte es aus, damit sich eine Datei nur per Download ansehen lässt. Das Team kann immer eine Vorschau öffnen.",
|
||||
"No downloads match these filters.": "Kein Download entspricht diesen Filtern.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Diese Datei hat noch niemand heruntergeladen oder in der Vorschau geöffnet.",
|
||||
"Preview :name": ":name in der Vorschau öffnen",
|
||||
"Previewed the file \":subject\" via the public group listing": "Hat die Datei \":subject\" über das öffentliche Gruppenverzeichnis in der Vorschau geöffnet",
|
||||
"Previews: :count": "Vorschauen: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Diese Installation baut ihre Images selbst, ein Pull ändert also nichts. Wenn das Release composer.lock oder das Frontend geändert hat, führe zusätzlich composer install und npm run build aus – die vollständige Anleitung steht in UPDATE.md.",
|
||||
"To update, run this in the directory you cloned:": "Zum Aktualisieren führe dies im geklonten Verzeichnis aus:",
|
||||
"View all previews (:count)": "Alle Vorschauen ansehen (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Dein Browser kann diese Datei nicht abspielen. Lade sie herunter, um sie anzusehen."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count descargas",
|
||||
":count file deleted.|:count files deleted.": ":count archivo eliminado.|:count archivos eliminados.",
|
||||
":count file imported.|:count files imported.": ":count archivo importado.|:count archivos importados.",
|
||||
":count files": ":count archivos",
|
||||
":count folders": ":count carpetas",
|
||||
":count items have been shared with you": "Se compartieron :count elementos contigo",
|
||||
":count members": ":count miembros",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(y :count error más)",
|
||||
"(and :count more errors)": "(y :count error más)|(y :count errores más)|(y :count errores más)",
|
||||
"(deleted account)": "(cuenta eliminada)",
|
||||
"0 means unlimited.": "0 significa sin límite.",
|
||||
"A client account request was approved": "Se aprobó una solicitud de cuenta de cliente",
|
||||
"A client account request was denied": "Se denegó una solicitud de cuenta de cliente",
|
||||
"A client left a group": "Un cliente salió de un grupo",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Se creó una nueva cuenta de cliente: :name (:email).",
|
||||
"A new client has registered": "Un nuevo cliente se ha registrado",
|
||||
"A new ProjectSend version is available": "Hay una nueva versión de ProjectSend disponible",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "La página de un grupo público siempre es accesible por su propio enlace una vez que lo marcás como público — esta sección solo controla la URL base y un directorio opcional que lista todos los grupos y archivos públicos a la vez.",
|
||||
"A public link was created": "Se creó un enlace público",
|
||||
"A public link was revoked": "Se revocó un enlace público",
|
||||
"A role was created": "Se creó un rol",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Administrador: se solicitó membresía a un grupo",
|
||||
"Admin: new client registered": "Administrador: nuevo cliente registrado",
|
||||
"Administration": "Administración",
|
||||
"After uploading you can assign it to clients and groups": "Después de subirlo podrás asignarlo a clientes y grupos",
|
||||
"Agree": "Aceptar",
|
||||
"All": "Todos",
|
||||
"All actions": "Todas las acciones",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Todos los comentarios",
|
||||
"All failed jobs deleted.": "Se eliminaron todos los trabajos fallidos.",
|
||||
"All files": "Todos los archivos",
|
||||
"All groups": "Todos los grupos",
|
||||
"All permissions": "Todos los permisos",
|
||||
"All rights reserved.": "Todos los derechos reservados.",
|
||||
"All roles": "Todos los roles",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Quien lo tenga ya no podrá descargar el archivo.",
|
||||
"Appearance": "Apariencia",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Se aplica a las páginas públicas de grupos/archivos y al portal de clientes \"Mis archivos\".",
|
||||
"Apply filters": "Aplicar filtros",
|
||||
"Approve": "Aprobar",
|
||||
"Approve account requests": "Aprobar solicitudes de cuenta",
|
||||
"Approve group membership requests": "Aprobar solicitudes de membresía a grupos",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "¿Seguro que quieres eliminar tu cuenta?",
|
||||
"Assign": "Asignar",
|
||||
"Assign categories to files": "Asignar categorías a archivos",
|
||||
"Assigned": "Asignado",
|
||||
"Assigned clients": "Clientes asignados",
|
||||
"Assigned the file \":subject\" to :target": "Asignó el archivo \":subject\" a :target",
|
||||
"Associate": "Asociar",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Autor",
|
||||
"Available groups": "Grupos disponibles",
|
||||
"Available placeholders": "Marcadores disponibles",
|
||||
"Back to My files": "Volver a Mis archivos",
|
||||
"Bad Gateway": "Mala puerta de enlace",
|
||||
"Bad Request": "Solicitud incorrecta",
|
||||
"Bandwidth Limit Exceeded": "Límite de ancho de banda excedido",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Cuerpo",
|
||||
"Browse": "Navegar",
|
||||
"Browse publicly shared groups and files below.": "Explorá los grupos y archivos compartidos públicamente a continuación.",
|
||||
"Built-in": "Integrado",
|
||||
"Built-in role names cannot be changed.": "Los nombres de los roles integrados no se pueden cambiar.",
|
||||
"Built-in roles cannot be deleted.": "Los roles integrados no se pueden eliminar.",
|
||||
"Call to action": "Llamado a la acción",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Confirmar contraseña",
|
||||
"Conflict": "Conflicto",
|
||||
"Connect": "Conectar",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Conecta un bucket externo — compatible con S3 (AWS S3, MinIO, Backblaze) o Google Cloud Storage — como almacenamiento para las subidas nuevas.",
|
||||
"Connection Closed Without Response": "Conexión cerrada sin respuesta",
|
||||
"Connection Timed Out": "Tiempo de conexión agotado",
|
||||
"Continue": "Continuar",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Desactiva tu cuenta ahora; se elimina permanentemente después de :days días",
|
||||
"Deactivated the account \":subject\"": "Desactivó la cuenta \":subject\"",
|
||||
"Default": "Predeterminado",
|
||||
"Defaults to the file name.": "Por defecto, el nombre del archivo.",
|
||||
"Delete": "Eliminar",
|
||||
"Delete :name": "Eliminar :name",
|
||||
"Delete account": "Eliminar cuenta",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Denegar",
|
||||
"Deny this request?": "¿Denegar esta solicitud?",
|
||||
"Description": "Descripción",
|
||||
"Description (optional)": "Descripción (opcional)",
|
||||
"Detach": "Desvincular",
|
||||
"Details": "Detalles",
|
||||
"Disable": "Deshabilitar",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Ir a la página :page",
|
||||
"Go to the login form": "Ir al formulario de inicio de sesión",
|
||||
"Gone": "Recurso no disponible",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Gris",
|
||||
"Green": "Verde",
|
||||
"Group": "Grupo",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Iniciar sesión",
|
||||
"Logout": "Finalizar sesión",
|
||||
"Loop Detected": "Bucle detectado",
|
||||
"Mail provider settings saved.": "Se guardó la configuración del proveedor de correo.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Modo de mantenimiento",
|
||||
"Manage custom fields": "Gestionar campos personalizados",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Aún no se han compartido archivos contigo.",
|
||||
"No files here yet.": "Todavía no hay archivos aquí.",
|
||||
"No files or folders match your search.": "Ningún archivo o carpeta coincide con tu búsqueda.",
|
||||
"No files yet. Upload the first one.": "Aún no hay archivos. Sube el primero.",
|
||||
"No folder": "Sin carpeta",
|
||||
"No groups match these filters.": "Ningún grupo coincide con estos filtros.",
|
||||
"No groups yet.": "Aún no hay grupos.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Aún no se agregaron destinatarios.",
|
||||
"No requests match your search.": "Ninguna solicitud coincide con tu búsqueda.",
|
||||
"No roles match this filter.": "Ningún rol coincide con este filtro.",
|
||||
"No service account key has been saved yet.": "Todavía no se ha guardado ninguna clave de cuenta de servicio.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Sin límite de tamaño. Las subidas pueden pausarse y se reanudan automáticamente tras interrupciones.",
|
||||
"No users match these filters.": "Ningún usuario coincide con estos filtros.",
|
||||
"No users yet.": "Aún no hay usuarios.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Naranja",
|
||||
"Origin Is Unreachable": "El origen es inalcanzable",
|
||||
"Outgoing email": "Correo saliente",
|
||||
"Outgoing email theme": "Tema del correo saliente",
|
||||
"Page :current of :last": "Página :current de :last",
|
||||
"Page Expired": "Página expirada",
|
||||
"Pagination Navigation": "Navegación por los enlaces de paginación",
|
||||
"Partial Content": "Contenido parcial",
|
||||
"Password": "Contraseña",
|
||||
"Password reset": "Restablecimiento de contraseña",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Pega el archivo de clave JSON de una cuenta de servicio con permiso de lectura y escritura de objetos en el bucket. Se guarda cifrado y no se vuelve a mostrar.",
|
||||
"Path": "Ruta",
|
||||
"Payload Too Large": "Solicitud demasiado grande",
|
||||
"Payment Required": "Pago requerido",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Configuración del listado público",
|
||||
"Public listing URL": "URL del listado público",
|
||||
"Public pages & portal": "Páginas públicas y portal",
|
||||
"Public pages and client portal theme": "Tema de las páginas públicas y el portal de clientes",
|
||||
"Published": "Publicado",
|
||||
"Purple": "Violeta",
|
||||
"Quota": "Cuota",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Restaurar",
|
||||
"Restore :name": "Restaurar :name",
|
||||
"Restricted extension": "Extensión restringida",
|
||||
"Result": "Resultado",
|
||||
"results": "resultados",
|
||||
"Review membership requests": "Revisar solicitudes de membresía",
|
||||
"Review pending requests": "Revisar solicitudes pendientes",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Nombre del rol",
|
||||
"Role updated.": "Rol actualizado.",
|
||||
"Roles": "Roles",
|
||||
"S3-compatible": "Compatible con S3",
|
||||
"Sample email": "Correo de muestra",
|
||||
"Save": "Guardar",
|
||||
"Save :name": "Guardar :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Envía un correo real de inmediato, sin pasar por la cola, para verificar que la configuración anterior funciona.",
|
||||
"Separate paragraphs with a blank line.": "Separa los párrafos con una línea en blanco.",
|
||||
"Server Error": "Error del servidor",
|
||||
"Service account key": "Clave de cuenta de servicio",
|
||||
"Service Unavailable": "Servicio no disponible",
|
||||
"Session Has Expired": "La sesión ha expirado",
|
||||
"Set file expiration dates": "Establecer fechas de vencimiento de archivos",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Ajustes",
|
||||
"Settings that apply to the whole installation": "Configuración que aplica a toda la instalación",
|
||||
"Setup complete": "Configuración completa",
|
||||
"Share a file with the people who manage your account.": "Comparte un archivo con las personas que administran tu cuenta.",
|
||||
"Share files with several clients at once": "Comparte archivos con varios clientes a la vez",
|
||||
"Shared the folder \":subject\" with :target": "Compartió la carpeta \":subject\" con :target",
|
||||
"Shared with": "Compartido con",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Protocolos de conmutación",
|
||||
"System": "Sistema",
|
||||
"System Administrator": "Administrador del sistema",
|
||||
"System settings": "Configuración del sistema",
|
||||
"System settings were updated": "Se actualizó la configuración del sistema",
|
||||
"System users": "Usuarios del sistema",
|
||||
"Tag": "Etiqueta",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Prueba",
|
||||
"Text": "Texto",
|
||||
"Text or author": "Texto o autor",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Esto no parece un archivo de clave de cuenta de servicio: no es JSON válido.",
|
||||
"That service account key file is missing its :field.": "A ese archivo de clave de cuenta de servicio le falta :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "La cuenta de :name se eliminará permanentemente. Esto no se puede deshacer.",
|
||||
"The account password was changed": "Se cambió la contraseña de la cuenta",
|
||||
"The account request of :name will be denied and the account deleted.": "La solicitud de cuenta de :name será denegada y la cuenta eliminada.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Los grupos a los que perteneces y a los que puedes pedir unirte",
|
||||
"The header/footer style used for every outgoing notification email.": "El estilo de encabezado/pie usado en todos los correos de notificación salientes.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "El aspecto de las páginas públicas, el portal de clientes y los correos de notificación salientes.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "El servidor de correo se configura en el archivo .env de esta instalación (variables MAIL_*), no aquí.",
|
||||
"The people who administer this installation and upload files": "Las personas que administran esta instalación y suben archivos",
|
||||
"The people you share files with": "Las personas con las que compartes archivos",
|
||||
"The provided two-factor authentication code was invalid.": "El código de autenticación de dos factores proporcionado no es válido.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Aún no hay nada para configurar.",
|
||||
"Test email from :site": "Correo de prueba de :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Estos archivos se eliminarán permanentemente del disco. Esto no se puede deshacer.",
|
||||
"Theme": "Tema",
|
||||
"Theme updated.": "Tema actualizado.",
|
||||
"Theming": "Temas",
|
||||
"Theming settings": "Configuración de temas",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Esta subida superaría tu cuota de almacenamiento de :quota MB.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Consejo: arrastra archivos y carpetas sobre una carpeta o una miga de pan para moverlos.",
|
||||
"Title": "Título",
|
||||
"Title (optional)": "Título (opcional)",
|
||||
"TLS": "TLS",
|
||||
"to": "al",
|
||||
"To": "Hasta",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Se han seleccionado demasiados archivos. Elige una selección más pequeña e inténtalo de nuevo.",
|
||||
"Too Many Requests": "Demasiadas peticiones",
|
||||
"Transactional email settings for this installation": "Configuración de correo transaccional para esta instalación",
|
||||
"Transfers — last 30 days": "Transferencias — últimos 30 días",
|
||||
"Translate": "Traducir",
|
||||
"Translate It": "Traducirlo",
|
||||
"Turns all transactional email for this installation on or off.": "Activa o desactiva todo el correo transaccional de esta instalación.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Subió el archivo \":subject\"",
|
||||
"Uploader": "Cargador",
|
||||
"Uploads": "Subidas",
|
||||
"Uploads (chart)": "Subidas",
|
||||
"URI Too Long": "URI demasiado largo",
|
||||
"URL slug": "Slug de URL",
|
||||
"Use a custom link instead of a random one": "Usar un enlace personalizado en lugar de uno aleatorio",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Esta es la única vez que se mostrará. Solo guardamos un hash, así que no podrá recuperarse después.",
|
||||
"Copy to clipboard": "Copiar al portapapeles",
|
||||
"Copied": "Copiado",
|
||||
"Your tokens": "Tus tokens",
|
||||
"Revoking a token takes effect immediately.": "Revocar un token surte efecto de inmediato.",
|
||||
"You have not created any tokens yet.": "Todavía no has creado ningún token.",
|
||||
"Expired": "Caducado",
|
||||
"Never used": "Nunca usado",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Gestionar campos personalizados de clientes",
|
||||
"Moderate comments": "Moderar comentarios",
|
||||
"Only me": "Solo yo",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Visible para cualquiera que pueda abrir este archivo, incluidos los visitantes sin sesión iniciada.",
|
||||
"Anyone, including visitors who are not logged in": "Cualquiera, incluidos los visitantes sin sesión iniciada",
|
||||
"Staff and clients": "Personal y clientes",
|
||||
"Staff only": "Solo el personal",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Los comentarios ya escritos siguen siendo legibles aunque un archivo deje de aceptar nuevos.",
|
||||
"Comments awaiting approval": "Comentarios esperando aprobación",
|
||||
"Comment settings": "Configuración de comentarios",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Comentarios dejados por visitantes que no iniciaron sesión. Nadie puede verlos hasta que los apruebes.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Conecta un bucket externo compatible con S3 (AWS S3, MinIO u otro servicio compatible) como almacenamiento para las subidas nuevas.",
|
||||
"Custom endpoint": "Endpoint personalizado",
|
||||
"Custom field created.": "Campo personalizado creado.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "No hay trabajos fallidos.",
|
||||
"No files match these filters.": "Ningún archivo coincide con estos filtros.",
|
||||
"None of the selected files could be edited.": "No se pudo editar ninguno de los archivos seleccionados.",
|
||||
"Nothing is waiting for approval.": "No hay nada esperando aprobación.",
|
||||
"of :count": "de :count",
|
||||
"of :quota": "de :quota",
|
||||
"on": "en",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Página",
|
||||
"Path prefix": "Prefijo de ruta",
|
||||
"Pending": "Pendiente",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Consulta cada tanto el repositorio público de projectsend.org por una versión más nueva y muestra un aviso en el panel. Nunca se descarga ni se aplica nada automáticamente.",
|
||||
"Permanently failed": "Falló definitivamente",
|
||||
"Post comment": "Publicar comentario",
|
||||
"Preview of :name": "Vista previa de :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Quitar la verificación en dos pasos",
|
||||
"Remove two-factor authentication?": "¿Quitar la verificación en dos pasos?",
|
||||
"Require a verified email address": "Exigir una dirección de correo verificada",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Ejecuta esto como el usuario con el que corre tu servidor web. INSTALL.md tiene el procedimiento completo.",
|
||||
"Score threshold": "Umbral de puntuación",
|
||||
"Search timezones…": "Buscar zonas horarias…",
|
||||
"Security check failed. Please try again.": "La comprobación de seguridad falló. Inténtalo de nuevo.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Este sitio está protegido por reCAPTCHA y se aplican la Política de Privacidad y los Términos de Servicio de Google.",
|
||||
"Those files have reached their download limit.": "Esos archivos han alcanzado su límite de descargas.",
|
||||
"Timezone": "Zona horaria",
|
||||
"To update, back up your database and files, then:": "Para actualizar, haz una copia de seguridad de tu base de datos y tus archivos, y luego:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Para actualizar, sigue los pasos de INSTALL.md: primero haz una copia de seguridad, luego descomprime la versión y ejecuta las migraciones.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Desactiva esto solo si este servidor no debe hacer peticiones salientes. Si no se puede contactar con el servicio, la contraseña se acepta igualmente, así que una instalación sin conexión nunca queda bloqueada por ello.",
|
||||
"Two-factor authentication removed.": "Verificación en dos pasos quitada.",
|
||||
"Two-factor authentication removed by an admin": "Verificación en dos pasos quitada por un administrador",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Tu propia cuenta está en :timezone, así que esto no cambiará cómo ves tú las fechas.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Tu propio inquilino, no «common». Microsoft permite que un usuario cambie la dirección de correo de su cuenta, así que un inicio de sesión solo es fiable cuando el token vino del inquilino que indicaste aquí.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Tu proveedor rechazó la clave secreta de esta instalación, así que los formularios protegidos se están dejando pasar sin comprobar. Revisa la clave secreta de abajo.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Tus formularios públicos están protegidos. Puedes cambiar a tus propias claves en cualquier momento."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Tus formularios públicos están protegidos. Puedes cambiar a tus propias claves en cualquier momento.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Si volviste a una versión anterior a propósito, ejecuta php artisan projectsend:update para que la base de datos coincida con el código que restauraste.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Pregunta antes de buscar una versión, antes de descargarla y antes de tocar tu instalación, y verifica la suma de comprobación de lo que descarga. UPDATE.md tiene el procedimiento completo.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Los archivos nuevos ya están en su lugar, pero la base de datos solo se actualizó hasta :applied. Hasta que ejecutes la actualización, esta instalación está corriendo código que no coincide con su base de datos.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Consulta cada tanto el repositorio público de projectsend.org por una versión más nueva y muestra un aviso en el panel. Nunca se descarga ni se aplica nada automáticamente: actualizar siempre es algo que haces tú, y el script de actualización pregunta antes de cada paso.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "La base de datos se actualizó a :applied, y PHP sigue sirviendo el código que había compilado antes de eso. Recargarlo hace desaparecer este aviso.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "La base de datos se actualizó a :applied, y este contenedor sigue ejecutando el código con el que arrancó.",
|
||||
"This server is running version :running, but version :applied is installed": "Este servidor está ejecutando la versión :running, pero la versión instalada es :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Para actualizar, ejecuta sudo ./update.sh en el directorio de instalación: pregunta antes de descargar o cambiar nada.",
|
||||
"To update, run this in the install directory:": "Para actualizar, ejecuta esto en el directorio de instalación:",
|
||||
"Version :running is installed, but the update has not been run": "La versión :running está instalada, pero la actualización no se ejecutó",
|
||||
"Updated ProjectSend to :to, from :from": "Actualizó ProjectSend a :to, desde :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend fue actualizado a una versión nueva",
|
||||
"Come and say hello": "Ven a saludarnos",
|
||||
"Continue to the dashboard": "Continuar al panel de control",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Únete a Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend tiene un Discord: novedades de cada versión, ayuda cuando algo no funciona como debería y otras personas que usan el mismo software. Nosotros también estamos ahí.",
|
||||
"What's new": "Novedades",
|
||||
"What each release brought to this installation.": "Lo que trajo cada versión a esta instalación.",
|
||||
"Add the rest of your team": "Añade al resto de tu equipo",
|
||||
"Add your first client": "Añade tu primer cliente",
|
||||
"Check the scheduler is running": "Comprueba que el programador de tareas esté funcionando",
|
||||
"Choose how your email looks": "Elige cómo se ve tu correo",
|
||||
"Choose how your file lists look": "Elige cómo se ven tus listas de archivos",
|
||||
"Drop one in and choose who gets it.": "Sube uno y elige quién lo recibe.",
|
||||
"Essential": "Imprescindible",
|
||||
"Expiring files and queued email need it.": "Los archivos que vencen y el correo en cola dependen de él.",
|
||||
"Four layouts for the client-facing pages.": "Cuatro diseños para las páginas que ven los clientes.",
|
||||
"Getting started": "Primeros pasos",
|
||||
"Group your clients": "Agrupa a tus clientes",
|
||||
"Made with care in Argentina. Shared with the world.": "Hecho con cariño en Argentina. Compartido con el mundo.",
|
||||
"Previewed on a real message.": "Previsualizado en un mensaje real.",
|
||||
"Resets and share links go out through it.": "Los restablecimientos y los enlaces compartidos salen por ahí.",
|
||||
"Set up your mail server": "Configura tu servidor de correo",
|
||||
"Share once instead of six times.": "Comparte una vez en lugar de seis.",
|
||||
"Staff accounts, each scoped by its role.": "Cuentas de personal, cada una limitada por su rol.",
|
||||
"Thank you for installing ProjectSend": "Gracias por instalar ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Gracias por actualizar ProjectSend",
|
||||
"The people you send files to.": "Las personas a las que les envías archivos.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "La versión corta de qué hacer en una instalación nueva. Nada de esto caduca.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Ahora estás en la :version y todo volvió a funcionar. Gracias por seguir confiando en ProjectSend para compartir tus archivos.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Ahora estás en la :version, desde la :previous, y todo volvió a funcionar. Gracias por seguir confiando en ProjectSend para compartir tus archivos.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Estás usando la versión :version, y es tuya. Esto es lo que conviene hacer primero.",
|
||||
"Thank you for choosing ProjectSend": "Gracias por elegir ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Tu sitio está listo, y mantenerlo funcionando es cosa nuestra. Esto es lo que conviene hacer primero.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 lo conserva todo. Las notificaciones sin leer no se eliminan nunca, por antiguas que sean.",
|
||||
":version is available": ":version está disponible",
|
||||
":version on :date": ":version el :date",
|
||||
"Check now": "Comprobar ahora",
|
||||
"Checked a moment ago, so this is the answer from then.": "La comprobación fue hace un momento, así que esta es la respuesta de entonces.",
|
||||
"Checking…": "Comprobando…",
|
||||
"Could not reach the update feed.": "No se pudo acceder al canal de actualizaciones.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Vuelca la base de datos antes de tocar nada, y detente si el volcado falla.",
|
||||
"Housekeeping": "Mantenimiento",
|
||||
"Keep failed jobs for": "Conservar los trabajos fallidos durante",
|
||||
"Keep read notifications for": "Conservar las notificaciones leídas durante",
|
||||
"Last checked :date": "Última comprobación: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "No tocar PHP ni el worker, para cuando los reinicies tú mismo.",
|
||||
"Other options": "Otras opciones",
|
||||
"Report which version is installed and which is available, and change nothing.": "Informa de qué versión está instalada y cuál está disponible, sin cambiar nada.",
|
||||
"Retention updated.": "Retención actualizada.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Si ejecutas el script sin opciones, te pregunta por cada una de estas cosas sobre la marcha. Estas son para las ejecuciones en las que ya sabes la respuesta.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Omite la pregunta sobre las copias de seguridad, para una ejecución que no estás supervisando.",
|
||||
"The latest release (:tag) is not a version this can compare.": "La última versión publicada (:tag) no es una versión que se pueda comparar aquí.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Dos cosas se acumulan solas. Estos son los plazos a los que se ajustan las purgas diarias de arriba.",
|
||||
"Up to date": "Al día",
|
||||
"Update checks are not available on this edition.": "Las comprobaciones de actualización no están disponibles en esta edición.",
|
||||
"Update options": "Opciones de actualización",
|
||||
"Updated": "Actualizado",
|
||||
"Version :version is available. You are running :current.": "La versión :version está disponible. Estás usando la :current.",
|
||||
"You are up to date, running version :current.": "Estás al día, con la versión :current.",
|
||||
"days": "días",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help enumera todas las opciones, y UPDATE.md explica todo el procedimiento, incluido hacerlo a mano.",
|
||||
"Fetch dashboard news": "Obtener noticias del panel",
|
||||
"Purge API request logs": "Purgar el registro de peticiones de API",
|
||||
"Purge erased accounts": "Purgar cuentas borradas",
|
||||
"Purge expired files": "Purgar archivos caducados",
|
||||
"Purge failed jobs": "Purgar trabajos fallidos",
|
||||
"Purge orphan files": "Purgar archivos huérfanos",
|
||||
"Purge read notifications": "Purgar notificaciones leídas",
|
||||
"Purge stale chunked uploads": "Purgar subidas por partes abandonadas",
|
||||
"Purge zip downloads": "Purgar descargas ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Coinciden con este filtro: :count, de la más reciente a la más antigua",
|
||||
"A file was previewed via the public group listing": "Se previsualizó un archivo mediante el listado público de grupos",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Cualquiera con el enlace puede abrir una imagen, un video, un audio o un PDF en el navegador en lugar de descargarlo. Desactívalo para que descargar sea la única forma de ver un archivo público.",
|
||||
"Clients can preview files": "Los clientes pueden previsualizar archivos",
|
||||
"Downloads & previews": "Descargas y previsualizaciones",
|
||||
"Downloads: :count": "Descargas: :count",
|
||||
"Let visitors preview public files": "Permitir que los visitantes previsualicen archivos públicos",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Permite que los clientes abran una imagen, un video, un audio o un PDF en el portal en lugar de solo descargarlo. Desactívalo para que descargar sea la única forma de ver un archivo. El personal siempre puede previsualizar.",
|
||||
"No downloads match these filters.": "Ninguna descarga coincide con estos filtros.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Todavía nadie descargó ni previsualizó este archivo.",
|
||||
"Preview :name": "Previsualizar :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Previsualizó el archivo \":subject\" mediante el listado público de grupos",
|
||||
"Previews: :count": "Previsualizaciones: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Esta instalación construye sus propias imágenes, así que descargar una no cambia nada. Si la versión modificó composer.lock o el frontend, ejecuta también composer install y npm run build: UPDATE.md tiene el procedimiento completo.",
|
||||
"To update, run this in the directory you cloned:": "Para actualizar, ejecuta esto en el directorio que clonaste:",
|
||||
"View all previews (:count)": "Ver todas las previsualizaciones (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Tu navegador no puede reproducir este archivo. Descárgalo para verlo."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count téléchargements",
|
||||
":count file deleted.|:count files deleted.": ":count fichier supprimé.|:count fichiers supprimés.",
|
||||
":count file imported.|:count files imported.": ":count fichier importé.|:count fichiers importés.",
|
||||
":count files": ":count fichiers",
|
||||
":count folders": ":count dossiers",
|
||||
":count items have been shared with you": ":count éléments ont été partagés avec vous",
|
||||
":count members": ":count membres",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(et :count erreur en plus)",
|
||||
"(and :count more errors)": "(et :count erreur en plus)|(et :count erreurs en plus)|(et :count erreurs en plus)",
|
||||
"(deleted account)": "(compte supprimé)",
|
||||
"0 means unlimited.": "0 signifie illimité.",
|
||||
"A client account request was approved": "Une demande de compte client a été approuvée",
|
||||
"A client account request was denied": "Une demande de compte client a été refusée",
|
||||
"A client left a group": "Un client a quitté un groupe",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Un nouveau compte client a été créé : :name (:email).",
|
||||
"A new client has registered": "Un nouveau client s'est inscrit",
|
||||
"A new ProjectSend version is available": "Une nouvelle version de ProjectSend est disponible",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "La page d'un groupe public reste toujours accessible par son propre lien dès que vous le marquez comme public — cette section ne contrôle que l'URL de base et un annuaire facultatif listant d'un coup tous les groupes et fichiers publics.",
|
||||
"A public link was created": "Un lien public a été créé",
|
||||
"A public link was revoked": "Un lien public a été révoqué",
|
||||
"A role was created": "Un rôle a été créé",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Administration : adhésion à un groupe demandée",
|
||||
"Admin: new client registered": "Administration : nouveau client inscrit",
|
||||
"Administration": "Administration",
|
||||
"After uploading you can assign it to clients and groups": "Après le téléversement, vous pourrez l'attribuer à des clients et à des groupes",
|
||||
"Agree": "Accepter",
|
||||
"All": "Tous",
|
||||
"All actions": "Toutes les actions",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Tous les commentaires",
|
||||
"All failed jobs deleted.": "Toutes les tâches échouées ont été supprimées.",
|
||||
"All files": "Tous les fichiers",
|
||||
"All groups": "Tous les groupes",
|
||||
"All permissions": "Toutes les permissions",
|
||||
"All rights reserved.": "Tous droits réservés.",
|
||||
"All roles": "Tous les rôles",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Quiconque l'utilise ne pourra plus télécharger le fichier.",
|
||||
"Appearance": "Apparence",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "S'applique aux pages publiques de groupes et de fichiers destinées aux visiteurs ainsi qu'au portail client « Mes fichiers ».",
|
||||
"Apply filters": "Appliquer les filtres",
|
||||
"Approve": "Approuver",
|
||||
"Approve account requests": "Approuver les demandes de compte",
|
||||
"Approve group membership requests": "Approuver les demandes d'adhésion aux groupes",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Voulez-vous vraiment supprimer votre compte ?",
|
||||
"Assign": "Attribuer",
|
||||
"Assign categories to files": "Attribuer des catégories aux fichiers",
|
||||
"Assigned": "Attribué",
|
||||
"Assigned clients": "Clients attribués",
|
||||
"Assigned the file \":subject\" to :target": "A attribué le fichier \":subject\" à :target",
|
||||
"Associate": "Associé",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Auteur",
|
||||
"Available groups": "Groupes disponibles",
|
||||
"Available placeholders": "Variables disponibles",
|
||||
"Back to My files": "Retour à Mes fichiers",
|
||||
"Bad Gateway": "Passerelle invalide",
|
||||
"Bad Request": "Requête erronée",
|
||||
"Bandwidth Limit Exceeded": "Limite de bande passante dépassée",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Corps du message",
|
||||
"Browse": "Parcourir",
|
||||
"Browse publicly shared groups and files below.": "Parcourez ci-dessous les groupes et fichiers partagés publiquement.",
|
||||
"Built-in": "Intégré",
|
||||
"Built-in role names cannot be changed.": "Le nom des rôles intégrés ne peut pas être modifié.",
|
||||
"Built-in roles cannot be deleted.": "Les rôles intégrés ne peuvent pas être supprimés.",
|
||||
"Call to action": "Appel à l'action",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Confirmer le mot de passe",
|
||||
"Conflict": "Conflit",
|
||||
"Connect": "Connecter",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Connectez un bucket externe — compatible S3 (AWS S3, MinIO, Backblaze) ou Google Cloud Storage — comme espace de stockage des nouveaux envois.",
|
||||
"Connection Closed Without Response": "Connexion fermée sans réponse",
|
||||
"Connection Timed Out": "La connexion a expiré",
|
||||
"Continue": "Continuer",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Désactivez votre compte maintenant ; il est effacé définitivement après :days jours",
|
||||
"Deactivated the account \":subject\"": "A désactivé le compte \":subject\"",
|
||||
"Default": "Par défaut",
|
||||
"Defaults to the file name.": "Reprend le nom du fichier par défaut.",
|
||||
"Delete": "Supprimer",
|
||||
"Delete :name": "Supprimer :name",
|
||||
"Delete account": "Supprimer le compte",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Refuser",
|
||||
"Deny this request?": "Refuser cette demande ?",
|
||||
"Description": "Description",
|
||||
"Description (optional)": "Description (facultatif)",
|
||||
"Detach": "Détacher",
|
||||
"Details": "Détails",
|
||||
"Disable": "Désactiver",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Aller à la page :page",
|
||||
"Go to the login form": "Aller au formulaire de connexion",
|
||||
"Gone": "Disparu",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Gris",
|
||||
"Green": "Vert",
|
||||
"Group": "Groupe",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Connexion",
|
||||
"Logout": "Déconnexion",
|
||||
"Loop Detected": "Boucle détectée",
|
||||
"Mail provider settings saved.": "Paramètres du fournisseur d'e-mail enregistrés.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Mode de maintenance",
|
||||
"Manage custom fields": "Gérer les champs personnalisés",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Aucun fichier n'a encore été partagé avec vous.",
|
||||
"No files here yet.": "Aucun fichier ici pour l'instant.",
|
||||
"No files or folders match your search.": "Aucun fichier ni dossier ne correspond à votre recherche.",
|
||||
"No files yet. Upload the first one.": "Aucun fichier pour l'instant. Téléversez le premier.",
|
||||
"No folder": "Aucun dossier",
|
||||
"No groups match these filters.": "Aucun groupe ne correspond à ces filtres.",
|
||||
"No groups yet.": "Aucun groupe pour l'instant.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Aucun destinataire ajouté pour l'instant.",
|
||||
"No requests match your search.": "Aucune demande ne correspond à votre recherche.",
|
||||
"No roles match this filter.": "Aucun rôle ne correspond à ce filtre.",
|
||||
"No service account key has been saved yet.": "Aucune clé de compte de service n'a encore été enregistrée.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Aucune limite de taille. Les téléversements peuvent être mis en pause et reprennent automatiquement après une interruption.",
|
||||
"No users match these filters.": "Aucun utilisateur ne correspond à ces filtres.",
|
||||
"No users yet.": "Aucun utilisateur pour l'instant.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Orange",
|
||||
"Origin Is Unreachable": "L'origine est inaccessible",
|
||||
"Outgoing email": "E-mails sortants",
|
||||
"Outgoing email theme": "Thème des e-mails sortants",
|
||||
"Page :current of :last": "Page :current sur :last",
|
||||
"Page Expired": "Page expirée",
|
||||
"Pagination Navigation": "Pagination",
|
||||
"Partial Content": "Contenu partiel",
|
||||
"Password": "Mot de passe",
|
||||
"Password reset": "Réinitialisation du mot de passe",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Collez le fichier de clé JSON d'un compte de service disposant d'un accès en lecture et écriture aux objets du bucket. Il est stocké chiffré et n'est plus jamais affiché.",
|
||||
"Path": "Chemin",
|
||||
"Payload Too Large": "Charge utile trop grande",
|
||||
"Payment Required": "Paiement requis",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Paramètres de l'annuaire public",
|
||||
"Public listing URL": "URL de l'annuaire public",
|
||||
"Public pages & portal": "Pages publiques et portail",
|
||||
"Public pages and client portal theme": "Thème des pages publiques et du portail client",
|
||||
"Published": "Publié",
|
||||
"Purple": "Violet",
|
||||
"Quota": "Quota",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Restaurer",
|
||||
"Restore :name": "Restaurer :name",
|
||||
"Restricted extension": "Extension restreinte",
|
||||
"Result": "Résultat",
|
||||
"results": "résultats",
|
||||
"Review membership requests": "Examiner les demandes d'adhésion",
|
||||
"Review pending requests": "Examiner les demandes en attente",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Nom du rôle",
|
||||
"Role updated.": "Rôle mis à jour.",
|
||||
"Roles": "Rôles",
|
||||
"S3-compatible": "Compatible S3",
|
||||
"Sample email": "Exemple d'e-mail",
|
||||
"Save": "Sauvegarder",
|
||||
"Save :name": "Sauvegarder :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Envoie immédiatement un vrai e-mail, sans passer par la file d'attente, pour vérifier que les paramètres ci-dessus fonctionnent réellement.",
|
||||
"Separate paragraphs with a blank line.": "Séparez les paragraphes par une ligne vide.",
|
||||
"Server Error": "Erreur serveur",
|
||||
"Service account key": "Clé de compte de service",
|
||||
"Service Unavailable": "Service indisponible",
|
||||
"Session Has Expired": "La session a expiré",
|
||||
"Set file expiration dates": "Définir des dates d'expiration des fichiers",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Paramètres",
|
||||
"Settings that apply to the whole installation": "Paramètres qui s'appliquent à toute l'installation",
|
||||
"Setup complete": "Configuration terminée",
|
||||
"Share a file with the people who manage your account.": "Partagez un fichier avec les personnes qui gèrent votre compte.",
|
||||
"Share files with several clients at once": "Partager des fichiers avec plusieurs clients à la fois",
|
||||
"Shared the folder \":subject\" with :target": "A partagé le dossier \":subject\" avec :target",
|
||||
"Shared with": "Partagé avec",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Protocoles de commutation",
|
||||
"System": "Système",
|
||||
"System Administrator": "Administrateur système",
|
||||
"System settings": "Paramètres système",
|
||||
"System settings were updated": "Les paramètres système ont été mis à jour",
|
||||
"System users": "Utilisateurs système",
|
||||
"Tag": "Mot clé",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Test",
|
||||
"Text": "Texte",
|
||||
"Text or author": "Texte ou auteur",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Cela ne ressemble pas à un fichier de clé de compte de service : ce n'est pas du JSON valide.",
|
||||
"That service account key file is missing its :field.": "Il manque :field à ce fichier de clé de compte de service.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Le compte de :name sera définitivement supprimé. Cette action est irréversible.",
|
||||
"The account password was changed": "Le mot de passe du compte a été modifié",
|
||||
"The account request of :name will be denied and the account deleted.": "La demande de compte de :name sera refusée et le compte supprimé.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Les groupes auxquels vous appartenez et ceux que vous pouvez demander à rejoindre",
|
||||
"The header/footer style used for every outgoing notification email.": "Le style d'en-tête et de pied de page utilisé pour chaque e-mail de notification sortant.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "L'apparence des pages publiques destinées aux visiteurs, du portail client et des e-mails de notification sortants.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Le serveur de messagerie lui-même se configure dans le fichier .env de cette installation (variables MAIL_*), pas ici.",
|
||||
"The people who administer this installation and upload files": "Les personnes qui administrent cette installation et téléversent des fichiers",
|
||||
"The people you share files with": "Les personnes avec qui vous partagez des fichiers",
|
||||
"The provided two-factor authentication code was invalid.": "Le code d'authentification à deux facteurs fourni était invalide.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Il n'y a rien à configurer pour l'instant.",
|
||||
"Test email from :site": "E-mail de test de :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Ces fichiers seront définitivement supprimés du disque. Cette action est irréversible.",
|
||||
"Theme": "Thème",
|
||||
"Theme updated.": "Thème mis à jour.",
|
||||
"Theming": "Thèmes",
|
||||
"Theming settings": "Paramètres des thèmes",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Ce téléversement dépasserait votre quota de stockage de :quota Mo.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Astuce : faites glisser fichiers et dossiers sur un dossier ou un fil d'Ariane pour les déplacer.",
|
||||
"Title": "Titre",
|
||||
"Title (optional)": "Titre (facultatif)",
|
||||
"TLS": "TLS",
|
||||
"to": "à",
|
||||
"To": "À",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Trop de fichiers sélectionnés. Réduisez votre sélection et réessayez.",
|
||||
"Too Many Requests": "Trop de requêtes",
|
||||
"Transactional email settings for this installation": "Paramètres des e-mails transactionnels de cette installation",
|
||||
"Transfers — last 30 days": "Transferts — 30 derniers jours",
|
||||
"Translate": "Traduire",
|
||||
"Translate It": "Traduis le",
|
||||
"Turns all transactional email for this installation on or off.": "Active ou désactive tous les e-mails transactionnels de cette installation.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "A téléversé le fichier \":subject\"",
|
||||
"Uploader": "Auteur du téléversement",
|
||||
"Uploads": "Téléversements",
|
||||
"Uploads (chart)": "Téléversements (graphique)",
|
||||
"URI Too Long": "URI trop long",
|
||||
"URL slug": "Slug d'URL",
|
||||
"Use a custom link instead of a random one": "Utiliser un lien personnalisé plutôt qu'un lien aléatoire",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "C'est la seule fois où il sera affiché. Nous n'en conservons qu'une empreinte : il ne pourra pas être récupéré ensuite.",
|
||||
"Copy to clipboard": "Copier dans le presse-papiers",
|
||||
"Copied": "Copié",
|
||||
"Your tokens": "Vos jetons",
|
||||
"Revoking a token takes effect immediately.": "La révocation d'un jeton prend effet immédiatement.",
|
||||
"You have not created any tokens yet.": "Vous n'avez encore créé aucun jeton.",
|
||||
"Expired": "Expiré",
|
||||
"Never used": "Jamais utilisé",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Gérer les champs personnalisés des clients",
|
||||
"Moderate comments": "Modérer les commentaires",
|
||||
"Only me": "Moi uniquement",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Visible par toute personne pouvant ouvrir ce fichier, y compris les visiteurs non connectés.",
|
||||
"Anyone, including visitors who are not logged in": "Tout le monde, y compris les visiteurs non connectés",
|
||||
"Staff and clients": "Équipe et clients",
|
||||
"Staff only": "Équipe uniquement",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Les commentaires déjà écrits restent lisibles même si un fichier cesse d'en accepter de nouveaux.",
|
||||
"Comments awaiting approval": "Commentaires en attente d'approbation",
|
||||
"Comment settings": "Paramètres des commentaires",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Commentaires laissés par des visiteurs non connectés. Personne ne peut les voir tant que vous ne les avez pas approuvés.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Connectez un bucket externe compatible S3 (AWS S3, MinIO ou un autre service compatible S3) comme espace de stockage des nouveaux téléversements.",
|
||||
"Custom endpoint": "Point de terminaison personnalisé",
|
||||
"Custom field created.": "Champ personnalisé créé.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Aucune tâche échouée.",
|
||||
"No files match these filters.": "Aucun fichier ne correspond à ces filtres.",
|
||||
"None of the selected files could be edited.": "Aucun des fichiers sélectionnés n'a pu être modifié.",
|
||||
"Nothing is waiting for approval.": "Rien n'est en attente d'approbation.",
|
||||
"of :count": "sur :count",
|
||||
"of :quota": "sur :quota",
|
||||
"on": "le",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Page",
|
||||
"Path prefix": "Préfixe de chemin",
|
||||
"Pending": "En attente",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Vérifie régulièrement si le dépôt public de projectsend.org propose une version plus récente et affiche un avis sur le tableau de bord. Rien n'est jamais téléchargé ni appliqué automatiquement.",
|
||||
"Permanently failed": "Échec définitif",
|
||||
"Post comment": "Publier le commentaire",
|
||||
"Preview of :name": "Aperçu de :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Supprimer l'authentification à deux facteurs",
|
||||
"Remove two-factor authentication?": "Supprimer l'authentification à deux facteurs ?",
|
||||
"Require a verified email address": "Exiger une adresse e-mail vérifiée",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Exécutez ces commandes en tant que l'utilisateur sous lequel tourne votre serveur web. INSTALL.md contient la procédure complète.",
|
||||
"Score threshold": "Seuil de score",
|
||||
"Search timezones…": "Rechercher des fuseaux horaires…",
|
||||
"Security check failed. Please try again.": "Le contrôle de sécurité a échoué. Veuillez réessayer.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Ce site est protégé par reCAPTCHA et la Politique de confidentialité et les Conditions d'utilisation de Google s'appliquent.",
|
||||
"Those files have reached their download limit.": "Ces fichiers ont atteint leur limite de téléchargements.",
|
||||
"Timezone": "Fuseau horaire",
|
||||
"To update, back up your database and files, then:": "Pour mettre à jour, sauvegardez votre base de données et vos fichiers, puis :",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Pour mettre à jour, suivez les étapes d'INSTALL.md : sauvegardez d'abord, puis décompressez la version et exécutez les migrations.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Ne désactivez ceci que si ce serveur ne doit faire aucune requête sortante. Si le service est injoignable, le mot de passe est accepté malgré tout, de sorte qu'une installation hors ligne n'est jamais bloquée par ce contrôle.",
|
||||
"Two-factor authentication removed.": "Authentification à deux facteurs supprimée.",
|
||||
"Two-factor authentication removed by an admin": "Authentification à deux facteurs supprimée par un administrateur",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Votre propre compte est réglé sur :timezone ; ceci ne changera donc pas l'affichage des dates pour vous.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Votre propre locataire, pas « common ». Microsoft permet à un utilisateur de changer l'adresse e-mail de son compte ; une connexion n'est donc fiable que lorsque le jeton provient du locataire que vous avez indiqué ici.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Votre fournisseur a rejeté la clé secrète de cette installation ; les formulaires protégés sont donc laissés passer sans contrôle. Vérifiez la clé secrète ci-dessous.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Vos formulaires publics sont protégés. Vous pouvez passer à vos propres clés à tout moment."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Vos formulaires publics sont protégés. Vous pouvez passer à vos propres clés à tout moment.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Si vous êtes revenu en arrière volontairement, exécutez php artisan projectsend:update pour que la base de données corresponde au code que vous avez restauré.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Il demande confirmation avant de chercher une version, avant d'en télécharger une et avant de toucher à votre installation — et vérifie la somme de contrôle de ce qu'il télécharge. UPDATE.md contient la procédure complète.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Les nouveaux fichiers sont en place, mais la base de données n'a été amenée qu'à :applied. Tant que la mise à jour n'a pas été lancée, cette installation exécute du code qui ne correspond pas à sa base de données.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Vérifie régulièrement si le dépôt public de projectsend.org propose une version plus récente et affiche un avis sur le tableau de bord. Rien n'est jamais téléchargé ni appliqué automatiquement : la mise à jour est toujours une action que vous lancez, et le script de mise à jour demande confirmation à chaque étape.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "La base de données a été mise à jour vers :applied, et PHP sert toujours le code qu’il avait compilé avant. Le recharger fait disparaître cet avis.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "La base de données a été mise à jour vers :applied, et ce conteneur exécute toujours le code avec lequel il a démarré.",
|
||||
"This server is running version :running, but version :applied is installed": "Ce serveur exécute la version :running, alors que la version :applied est installée",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Pour mettre à jour, exécutez sudo ./update.sh dans le répertoire d'installation — il demande confirmation avant de télécharger ou de modifier quoi que ce soit.",
|
||||
"To update, run this in the install directory:": "Pour mettre à jour, exécutez ceci dans le répertoire d'installation :",
|
||||
"Version :running is installed, but the update has not been run": "La version :running est installée, mais la mise à jour n'a pas été lancée",
|
||||
"Updated ProjectSend to :to, from :from": "A mis à jour ProjectSend vers :to, depuis :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend a été mis à jour vers une nouvelle version",
|
||||
"Come and say hello": "Venez nous dire bonjour",
|
||||
"Continue to the dashboard": "Continuer vers le tableau de bord",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Rejoindre le Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend a un Discord : les nouveautés de chaque version, de l'aide quand quelque chose ne se comporte pas comme prévu, et d'autres personnes qui utilisent le même logiciel. Nous y sommes aussi.",
|
||||
"What's new": "Nouveautés",
|
||||
"What each release brought to this installation.": "Ce que chaque version a apporté à cette installation.",
|
||||
"Add the rest of your team": "Ajoutez le reste de votre équipe",
|
||||
"Add your first client": "Ajoutez votre premier client",
|
||||
"Check the scheduler is running": "Vérifiez que le planificateur tourne",
|
||||
"Choose how your email looks": "Choisissez l'apparence de vos e-mails",
|
||||
"Choose how your file lists look": "Choisissez l'apparence de vos listes de fichiers",
|
||||
"Drop one in and choose who gets it.": "Déposez-en un et choisissez qui le reçoit.",
|
||||
"Essential": "Indispensable",
|
||||
"Expiring files and queued email need it.": "Les fichiers qui expirent et les e-mails en attente en dépendent.",
|
||||
"Four layouts for the client-facing pages.": "Quatre mises en page pour les pages que voient les clients.",
|
||||
"Getting started": "Premiers pas",
|
||||
"Group your clients": "Regroupez vos clients",
|
||||
"Made with care in Argentina. Shared with the world.": "Fait avec soin en Argentine. Partagé avec le monde.",
|
||||
"Previewed on a real message.": "Prévisualisé sur un vrai message.",
|
||||
"Resets and share links go out through it.": "Les réinitialisations et les liens de partage passent par là.",
|
||||
"Set up your mail server": "Configurez votre serveur de messagerie",
|
||||
"Share once instead of six times.": "Partagez une fois plutôt que six.",
|
||||
"Staff accounts, each scoped by its role.": "Des comptes d'équipe, chacun limité par son rôle.",
|
||||
"Thank you for installing ProjectSend": "Merci d'avoir installé ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Merci d'avoir mis à jour ProjectSend",
|
||||
"The people you send files to.": "Les personnes à qui vous envoyez des fichiers.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "La version courte de ce qu'il faut faire sur une nouvelle installation. Rien de tout cela n'expire.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Vous êtes maintenant en :version, et tout est reparti. Merci de continuer à confier vos fichiers à ProjectSend.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Vous êtes maintenant en :version, contre :previous auparavant, et tout est reparti. Merci de continuer à confier vos fichiers à ProjectSend.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Vous utilisez la version :version, et elle est à vous. Voici ce qui vaut la peine d'être fait en premier.",
|
||||
"Thank you for choosing ProjectSend": "Merci d'avoir choisi ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Votre site est prêt, et le faire tourner, c'est notre travail. Voici ce qui vaut la peine d'être fait en premier.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 conserve tout. Les notifications non lues ne sont jamais supprimées, quel que soit leur âge.",
|
||||
":version is available": ":version est disponible",
|
||||
":version on :date": ":version le :date",
|
||||
"Check now": "Vérifier maintenant",
|
||||
"Checked a moment ago, so this is the answer from then.": "La vérification date d'il y a un instant : voici la réponse d'alors.",
|
||||
"Checking…": "Vérification…",
|
||||
"Could not reach the update feed.": "Impossible de joindre le flux des mises à jour.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Faire un dump de la base de données avant de toucher à quoi que ce soit, et s'arrêter si le dump échoue.",
|
||||
"Housekeeping": "Entretien",
|
||||
"Keep failed jobs for": "Conserver les tâches échouées pendant",
|
||||
"Keep read notifications for": "Conserver les notifications lues pendant",
|
||||
"Last checked :date": "Dernière vérification : :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Ne pas toucher à PHP ni au worker, pour quand vous les redémarrez vous-même.",
|
||||
"Other options": "Autres options",
|
||||
"Report which version is installed and which is available, and change nothing.": "Indique quelle version est installée et laquelle est disponible, sans rien changer.",
|
||||
"Retention updated.": "Rétention mise à jour.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Lancé sans options, le script pose chacune de ces questions au fur et à mesure. Celles-ci servent aux exécutions où vous connaissez déjà la réponse.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Passe la question de la sauvegarde, pour une exécution que vous ne surveillez pas.",
|
||||
"The latest release (:tag) is not a version this can compare.": "La dernière version publiée (:tag) n'est pas une version comparable ici.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Deux choses s'accumulent toutes seules. Voici les délais auxquels se tiennent les purges quotidiennes ci-dessus.",
|
||||
"Up to date": "À jour",
|
||||
"Update checks are not available on this edition.": "Les vérifications de mise à jour ne sont pas disponibles dans cette édition.",
|
||||
"Update options": "Options de mise à jour",
|
||||
"Updated": "Mis à jour",
|
||||
"Version :version is available. You are running :current.": "La version :version est disponible. Vous utilisez la :current.",
|
||||
"You are up to date, running version :current.": "Vous êtes à jour, avec la version :current.",
|
||||
"days": "jours",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help liste toutes les options, et UPDATE.md détaille toute la procédure, y compris à la main.",
|
||||
"Fetch dashboard news": "Récupérer les actualités du tableau de bord",
|
||||
"Purge API request logs": "Purger le journal des requêtes API",
|
||||
"Purge erased accounts": "Purger les comptes effacés",
|
||||
"Purge expired files": "Purger les fichiers expirés",
|
||||
"Purge failed jobs": "Purger les tâches échouées",
|
||||
"Purge orphan files": "Purger les fichiers orphelins",
|
||||
"Purge read notifications": "Purger les notifications lues",
|
||||
"Purge stale chunked uploads": "Purger les envois par morceaux abandonnés",
|
||||
"Purge zip downloads": "Purger les téléchargements ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Correspondances pour ce filtre : :count, de la plus récente à la plus ancienne",
|
||||
"A file was previewed via the public group listing": "Un fichier a été prévisualisé via l'annuaire public des groupes",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Toute personne disposant du lien peut ouvrir une image, une vidéo, un fichier audio ou un PDF dans le navigateur au lieu de le télécharger. Désactivez l'option pour que le téléchargement soit le seul moyen de voir un fichier public.",
|
||||
"Clients can preview files": "Les clients peuvent prévisualiser les fichiers",
|
||||
"Downloads & previews": "Téléchargements et aperçus",
|
||||
"Downloads: :count": "Téléchargements : :count",
|
||||
"Let visitors preview public files": "Autoriser les visiteurs à prévisualiser les fichiers publics",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Permet aux clients d'ouvrir une image, une vidéo, un fichier audio ou un PDF dans le portail au lieu de seulement le télécharger. Désactivez l'option pour que le téléchargement soit le seul moyen de voir un fichier. L'équipe peut toujours prévisualiser.",
|
||||
"No downloads match these filters.": "Aucun téléchargement ne correspond à ces filtres.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Personne n'a encore téléchargé ni prévisualisé ce fichier.",
|
||||
"Preview :name": "Prévisualiser :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "A prévisualisé le fichier \":subject\" via l'annuaire public des groupes",
|
||||
"Previews: :count": "Aperçus : :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Cette installation construit ses propres images : en télécharger une ne change rien. Si la version a modifié composer.lock ou le frontend, exécutez aussi composer install et npm run build — UPDATE.md décrit la procédure complète.",
|
||||
"To update, run this in the directory you cloned:": "Pour mettre à jour, exécutez ceci dans le répertoire que vous avez cloné :",
|
||||
"View all previews (:count)": "Voir tous les aperçus (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Votre navigateur ne peut pas lire ce fichier. Téléchargez-le pour le consulter."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count unduhan",
|
||||
":count file deleted.|:count files deleted.": ":count berkas dihapus.|:count berkas dihapus.",
|
||||
":count file imported.|:count files imported.": ":count berkas diimpor.|:count berkas diimpor.",
|
||||
":count files": ":count berkas",
|
||||
":count folders": ":count folder",
|
||||
":count items have been shared with you": ":count item telah dibagikan kepada Anda",
|
||||
":count members": ":count anggota",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(dan :count kesalahan lainnya)",
|
||||
"(and :count more errors)": "(dan :count kesalahan lainnya)|(dan :count kesalahan lainnya)|(dan :count kesalahan lainnya)",
|
||||
"(deleted account)": "(akun terhapus)",
|
||||
"0 means unlimited.": "0 berarti tanpa batas.",
|
||||
"A client account request was approved": "Permintaan akun klien disetujui",
|
||||
"A client account request was denied": "Permintaan akun klien ditolak",
|
||||
"A client left a group": "Seorang klien keluar dari grup",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Akun klien baru telah dibuat: :name (:email).",
|
||||
"A new client has registered": "Klien baru telah mendaftar",
|
||||
"A new ProjectSend version is available": "Tersedia versi ProjectSend yang baru",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Begitu Anda menandai sebuah grup sebagai publik, halamannya selalu bisa diakses lewat tautannya sendiri — bagian ini hanya mengatur URL dasar dan halaman direktori opsional yang menampilkan semua grup dan berkas publik sekaligus.",
|
||||
"A public link was created": "Sebuah tautan publik dibuat",
|
||||
"A public link was revoked": "Sebuah tautan publik dicabut",
|
||||
"A role was created": "Sebuah peran dibuat",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Admin: ada permintaan keanggotaan grup",
|
||||
"Admin: new client registered": "Admin: klien baru mendaftar",
|
||||
"Administration": "Administrasi",
|
||||
"After uploading you can assign it to clients and groups": "Setelah diunggah, Anda bisa menetapkannya ke klien dan grup",
|
||||
"Agree": "Setuju",
|
||||
"All": "Semua",
|
||||
"All actions": "Semua tindakan",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Semua komentar",
|
||||
"All failed jobs deleted.": "Semua tugas yang gagal telah dihapus.",
|
||||
"All files": "Semua berkas",
|
||||
"All groups": "Semua grup",
|
||||
"All permissions": "Semua izin",
|
||||
"All rights reserved.": "Hak Cipta Dilindungi.",
|
||||
"All roles": "Semua peran",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Siapa pun yang memakainya tidak akan bisa lagi mengunduh berkas tersebut.",
|
||||
"Appearance": "Tampilan",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Berlaku untuk halaman grup dan berkas publik bagi pengunjung serta portal klien \"Berkas saya\".",
|
||||
"Apply filters": "Terapkan filter",
|
||||
"Approve": "Setujui",
|
||||
"Approve account requests": "Menyetujui permintaan akun",
|
||||
"Approve group membership requests": "Menyetujui permintaan keanggotaan grup",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Yakin ingin menghapus akun Anda?",
|
||||
"Assign": "Menetapkan",
|
||||
"Assign categories to files": "Menetapkan kategori ke berkas",
|
||||
"Assigned": "Ditetapkan",
|
||||
"Assigned clients": "Klien yang ditangani",
|
||||
"Assigned the file \":subject\" to :target": "Menetapkan berkas \":subject\" untuk :target",
|
||||
"Associate": "Rekan",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Penulis",
|
||||
"Available groups": "Grup yang tersedia",
|
||||
"Available placeholders": "Placeholder yang tersedia",
|
||||
"Back to My files": "Kembali ke Berkas saya",
|
||||
"Bad Gateway": "Gateway buruk",
|
||||
"Bad Request": "Permintaan yang buruk",
|
||||
"Bandwidth Limit Exceeded": "Batas Bandwidth terlampaui",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Isi pesan",
|
||||
"Browse": "Jelajahi",
|
||||
"Browse publicly shared groups and files below.": "Jelajahi grup dan berkas yang dibagikan secara publik di bawah ini.",
|
||||
"Built-in": "Bawaan",
|
||||
"Built-in role names cannot be changed.": "Nama peran bawaan tidak bisa diubah.",
|
||||
"Built-in roles cannot be deleted.": "Peran bawaan tidak bisa dihapus.",
|
||||
"Call to action": "Ajakan bertindak",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Konfirmasi kata sandi",
|
||||
"Conflict": "Konflik",
|
||||
"Connect": "Menghubung",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Hubungkan bucket eksternal — kompatibel dengan S3 (AWS S3, MinIO, Backblaze) atau Google Cloud Storage — sebagai penyimpanan untuk unggahan baru.",
|
||||
"Connection Closed Without Response": "Koneksi Ditutup Tanpa Respon",
|
||||
"Connection Timed Out": "Waktu koneksi berakhir",
|
||||
"Continue": "Lanjut",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Nonaktifkan akun Anda sekarang; setelah :days hari akan dihapus permanen",
|
||||
"Deactivated the account \":subject\"": "Menonaktifkan akun \":subject\"",
|
||||
"Default": "Bawaan",
|
||||
"Defaults to the file name.": "Secara bawaan memakai nama berkas.",
|
||||
"Delete": "Hapus",
|
||||
"Delete :name": "Hapus :name",
|
||||
"Delete account": "Hapus akun",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Tolak",
|
||||
"Deny this request?": "Tolak permintaan ini?",
|
||||
"Description": "Deskripsi",
|
||||
"Description (optional)": "Deskripsi (opsional)",
|
||||
"Detach": "Melepaskan",
|
||||
"Details": "Detail",
|
||||
"Disable": "Matikan",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Ke halaman :page",
|
||||
"Go to the login form": "Ke formulir masuk",
|
||||
"Gone": "Hilang",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Abu-abu",
|
||||
"Green": "Hijau",
|
||||
"Group": "Grup",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Masuk",
|
||||
"Logout": "Keluar",
|
||||
"Loop Detected": "Terdeteksi perulangan",
|
||||
"Mail provider settings saved.": "Pengaturan penyedia email disimpan.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Mode pemeliharaan",
|
||||
"Manage custom fields": "Mengelola bidang kustom",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Belum ada berkas yang dibagikan kepada Anda.",
|
||||
"No files here yet.": "Belum ada berkas di sini.",
|
||||
"No files or folders match your search.": "Tidak ada berkas atau folder yang cocok dengan pencarian Anda.",
|
||||
"No files yet. Upload the first one.": "Belum ada berkas. Unggah yang pertama.",
|
||||
"No folder": "Tanpa folder",
|
||||
"No groups match these filters.": "Tidak ada grup yang cocok dengan filter ini.",
|
||||
"No groups yet.": "Belum ada grup.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Belum ada penerima yang ditambahkan.",
|
||||
"No requests match your search.": "Tidak ada permintaan yang cocok dengan pencarian Anda.",
|
||||
"No roles match this filter.": "Tidak ada peran yang cocok dengan filter ini.",
|
||||
"No service account key has been saved yet.": "Belum ada kunci akun layanan yang disimpan.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Tanpa batas ukuran. Unggahan bisa dijeda dan otomatis berlanjut setelah terputus.",
|
||||
"No users match these filters.": "Tidak ada pengguna yang cocok dengan filter ini.",
|
||||
"No users yet.": "Belum ada pengguna.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Oranye",
|
||||
"Origin Is Unreachable": "Asal tidak terjangkau",
|
||||
"Outgoing email": "Email keluar",
|
||||
"Outgoing email theme": "Tema email keluar",
|
||||
"Page :current of :last": "Halaman :current dari :last",
|
||||
"Page Expired": "Halaman Kadaluwarsa",
|
||||
"Pagination Navigation": "Navigasi Paginasi",
|
||||
"Partial Content": "Konten parsial",
|
||||
"Password": "Kata sandi",
|
||||
"Password reset": "Penyetelan ulang kata sandi",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Tempel berkas kunci JSON milik akun layanan yang punya akses baca dan tulis ke objek di bucket. Berkas disimpan terenkripsi dan tidak pernah ditampilkan lagi.",
|
||||
"Path": "Jalur",
|
||||
"Payload Too Large": "Payload terlalu besar",
|
||||
"Payment Required": "Pembayaran Diperlukan",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Pengaturan daftar publik",
|
||||
"Public listing URL": "URL daftar publik",
|
||||
"Public pages & portal": "Halaman publik & portal",
|
||||
"Public pages and client portal theme": "Tema halaman publik dan portal klien",
|
||||
"Published": "Diterbitkan",
|
||||
"Purple": "Ungu",
|
||||
"Quota": "Kuota",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Pulihkan",
|
||||
"Restore :name": "Pulihkan :name",
|
||||
"Restricted extension": "Ekstensi yang dibatasi",
|
||||
"Result": "Hasil",
|
||||
"results": "hasil",
|
||||
"Review membership requests": "Meninjau permintaan keanggotaan",
|
||||
"Review pending requests": "Tinjau permintaan yang tertunda",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Nama peran",
|
||||
"Role updated.": "Peran diperbarui.",
|
||||
"Roles": "Peran",
|
||||
"S3-compatible": "Kompatibel dengan S3",
|
||||
"Sample email": "Contoh email",
|
||||
"Save": "Simpan",
|
||||
"Save :name": "Hemat :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Mengirim email sungguhan sekarang juga tanpa lewat antrean, agar Anda bisa memastikan pengaturan di atas benar-benar bekerja.",
|
||||
"Separate paragraphs with a blank line.": "Pisahkan paragraf dengan satu baris kosong.",
|
||||
"Server Error": "Terjadi Kesalahan Server",
|
||||
"Service account key": "Kunci akun layanan",
|
||||
"Service Unavailable": "Layanan Tidak Tersedia",
|
||||
"Session Has Expired": "Sesi telah berakhir",
|
||||
"Set file expiration dates": "Menetapkan tanggal kedaluwarsa berkas",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Pengaturan",
|
||||
"Settings that apply to the whole installation": "Pengaturan yang berlaku untuk seluruh instalasi",
|
||||
"Setup complete": "Penyiapan selesai",
|
||||
"Share a file with the people who manage your account.": "Bagikan berkas kepada orang-orang yang mengelola akun Anda.",
|
||||
"Share files with several clients at once": "Bagikan berkas ke beberapa klien sekaligus",
|
||||
"Shared the folder \":subject\" with :target": "Membagikan folder \":subject\" kepada :target",
|
||||
"Shared with": "Dibagikan kepada",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Beralih protokol",
|
||||
"System": "Sistem",
|
||||
"System Administrator": "Administrator sistem",
|
||||
"System settings": "Pengaturan sistem",
|
||||
"System settings were updated": "Pengaturan sistem diperbarui",
|
||||
"System users": "Pengguna sistem",
|
||||
"Tag": "Menandai",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Uji",
|
||||
"Text": "Teks",
|
||||
"Text or author": "Isi atau penulis",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Ini sepertinya bukan berkas kunci akun layanan: bukan JSON yang valid.",
|
||||
"That service account key file is missing its :field.": "Berkas kunci akun layanan ini tidak memiliki :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Akun :name akan dihapus permanen. Tindakan ini tidak bisa dibatalkan.",
|
||||
"The account password was changed": "Kata sandi akun diubah",
|
||||
"The account request of :name will be denied and the account deleted.": "Permintaan akun dari :name akan ditolak dan akunnya dihapus.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Grup yang Anda ikuti dan grup yang bisa Anda minta untuk bergabung",
|
||||
"The header/footer style used for every outgoing notification email.": "Gaya kepala dan kaki surat yang dipakai di setiap email notifikasi keluar.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Tampilan halaman publik bagi pengunjung, portal klien, dan email notifikasi keluar.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Server email itu sendiri dikonfigurasi di berkas .env instalasi ini (variabel MAIL_*), bukan di sini.",
|
||||
"The people who administer this installation and upload files": "Orang-orang yang mengelola instalasi ini dan mengunggah berkas",
|
||||
"The people you share files with": "Orang-orang yang Anda kirimi berkas",
|
||||
"The provided two-factor authentication code was invalid.": "Kode autentikasi dua faktor yang dimasukkan tidak sah.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Belum ada yang perlu dikonfigurasi.",
|
||||
"Test email from :site": "Email uji dari :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Berkas-berkas ini akan dihapus permanen dari disk. Tindakan ini tidak bisa dibatalkan.",
|
||||
"Theme": "Tema",
|
||||
"Theme updated.": "Tema diperbarui.",
|
||||
"Theming": "Tema",
|
||||
"Theming settings": "Pengaturan tema",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Unggahan ini akan melampaui kuota penyimpanan Anda sebesar :quota MB.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Tips: seret berkas dan folder ke sebuah folder atau ke jalur navigasi untuk memindahkannya.",
|
||||
"Title": "Judul",
|
||||
"Title (optional)": "Judul (opsional)",
|
||||
"TLS": "TLS",
|
||||
"to": "kepada",
|
||||
"To": "Kepada",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Terlalu banyak berkas yang dipilih. Kurangi pilihan lalu coba lagi.",
|
||||
"Too Many Requests": "Terlalu Banyak Permintaan",
|
||||
"Transactional email settings for this installation": "Pengaturan email transaksional untuk instalasi ini",
|
||||
"Transfers — last 30 days": "Transfer — 30 hari terakhir",
|
||||
"Translate": "Menerjemahkan",
|
||||
"Translate It": "Terjemahkan",
|
||||
"Turns all transactional email for this installation on or off.": "Menyalakan atau mematikan seluruh email transaksional instalasi ini.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Mengunggah berkas \":subject\"",
|
||||
"Uploader": "Pengunggah",
|
||||
"Uploads": "Unggahan",
|
||||
"Uploads (chart)": "Unggahan (grafik)",
|
||||
"URI Too Long": "URI terlalu panjang",
|
||||
"URL slug": "Slug URL",
|
||||
"Use a custom link instead of a random one": "Gunakan tautan pilihan sendiri, bukan tautan acak",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Token hanya ditampilkan kali ini. Kami hanya menyimpan hash-nya, jadi tidak bisa dipulihkan nanti.",
|
||||
"Copy to clipboard": "Salin ke papan klip",
|
||||
"Copied": "Tersalin",
|
||||
"Your tokens": "Token Anda",
|
||||
"Revoking a token takes effect immediately.": "Pencabutan token langsung berlaku.",
|
||||
"You have not created any tokens yet.": "Anda belum membuat token apa pun.",
|
||||
"Expired": "Kedaluwarsa",
|
||||
"Never used": "Belum pernah dipakai",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Mengelola bidang kustom klien",
|
||||
"Moderate comments": "Memoderasi komentar",
|
||||
"Only me": "Hanya saya",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Terlihat oleh siapa pun yang bisa membuka berkas ini, termasuk pengunjung yang belum masuk.",
|
||||
"Anyone, including visitors who are not logged in": "Siapa saja, termasuk pengunjung yang belum masuk",
|
||||
"Staff and clients": "Staf dan klien",
|
||||
"Staff only": "Hanya staf",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Komentar yang sudah ditulis tetap bisa dibaca, meskipun berkas berhenti menerima komentar baru.",
|
||||
"Comments awaiting approval": "Komentar yang menunggu persetujuan",
|
||||
"Comment settings": "Pengaturan komentar",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Komentar dari pengunjung yang belum masuk. Tidak ada yang bisa melihatnya sampai Anda menyetujuinya.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Hubungkan bucket eksternal yang kompatibel dengan S3 (AWS S3, MinIO, atau layanan lain yang kompatibel) sebagai tempat penyimpanan unggahan baru.",
|
||||
"Custom endpoint": "Endpoint kustom",
|
||||
"Custom field created.": "Bidang kustom dibuat.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Tidak ada tugas yang gagal.",
|
||||
"No files match these filters.": "Tidak ada berkas yang cocok dengan filter ini.",
|
||||
"None of the selected files could be edited.": "Tidak satu pun berkas yang dipilih dapat diubah.",
|
||||
"Nothing is waiting for approval.": "Tidak ada yang menunggu persetujuan.",
|
||||
"of :count": "dari :count",
|
||||
"of :quota": "dari :quota",
|
||||
"on": "pada",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Halaman",
|
||||
"Path prefix": "Awalan jalur",
|
||||
"Pending": "Tertunda",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Secara berkala memeriksa repositori publik projectsend.org untuk versi yang lebih baru dan menampilkan pemberitahuan di dasbor. Tidak ada yang pernah diunduh atau dipasang secara otomatis.",
|
||||
"Permanently failed": "Gagal permanen",
|
||||
"Post comment": "Kirim komentar",
|
||||
"Preview of :name": "Pratinjau :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Hapus autentikasi dua faktor",
|
||||
"Remove two-factor authentication?": "Hapus autentikasi dua faktor?",
|
||||
"Require a verified email address": "Wajibkan alamat email yang terverifikasi",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Jalankan ini sebagai pengguna yang menjalankan server web Anda. INSTALL.md memuat prosedur lengkapnya.",
|
||||
"Score threshold": "Ambang skor",
|
||||
"Search timezones…": "Cari zona waktu…",
|
||||
"Security check failed. Please try again.": "Pemeriksaan keamanan gagal. Silakan coba lagi.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Situs ini dilindungi oleh reCAPTCHA dan Kebijakan Privasi serta Ketentuan Layanan Google berlaku.",
|
||||
"Those files have reached their download limit.": "Berkas-berkas itu telah mencapai batas unduhannya.",
|
||||
"Timezone": "Zona waktu",
|
||||
"To update, back up your database and files, then:": "Untuk memperbarui, cadangkan basis data dan berkas Anda, lalu:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Untuk memperbarui, ikuti langkah-langkah di INSTALL.md — cadangkan dahulu, lalu bongkar rilisnya dan jalankan migrasinya.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Nonaktifkan ini hanya jika server ini tidak boleh membuat permintaan keluar. Jika layanan tidak dapat dihubungi, kata sandi tetap diterima, jadi pemasangan luring tidak pernah terhalang olehnya.",
|
||||
"Two-factor authentication removed.": "Autentikasi dua faktor dihapus.",
|
||||
"Two-factor authentication removed by an admin": "Autentikasi dua faktor dihapus oleh admin",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Akun Anda sendiri diatur ke :timezone, jadi ini tidak akan mengubah tampilan tanggal bagi Anda.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Penyewa Anda sendiri, bukan \"common\". Microsoft mengizinkan pengguna mengubah alamat email pada akunnya, jadi sebuah masuk hanya tepercaya jika tokennya berasal dari penyewa yang Anda sebutkan di sini.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Penyedia Anda menolak kunci rahasia pemasangan ini, jadi formulir yang dilindungi diloloskan tanpa pemeriksaan. Periksa kunci rahasia di bawah.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Formulir publik Anda terlindungi. Anda dapat beralih ke kunci Anda sendiri kapan saja."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Formulir publik Anda terlindungi. Anda dapat beralih ke kunci Anda sendiri kapan saja.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Jika Anda sengaja kembali ke versi sebelumnya, jalankan php artisan projectsend:update agar basis data sesuai dengan kode yang Anda pulihkan.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Skrip ini bertanya sebelum memeriksa rilis, sebelum mengunduhnya, dan sebelum menyentuh instalasi Anda — serta memverifikasi checksum berkas yang diunduh. UPDATE.md memuat prosedur lengkapnya.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Berkas baru sudah di tempatnya, tetapi basis data baru dibawa sampai :applied. Sampai pembaruan dijalankan, instalasi ini menjalankan kode yang tidak cocok dengan basis datanya.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Secara berkala memeriksa repositori publik projectsend.org untuk versi yang lebih baru dan menampilkan pemberitahuan di dasbor. Tidak ada yang pernah diunduh atau dipasang secara otomatis — pembaruan selalu Anda yang menjalankan, dan skrip pembaruan bertanya sebelum setiap langkah.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Basis data telah diperbarui ke :applied, sementara PHP masih menyajikan kode yang dikompilasi sebelumnya. Memuat ulang PHP akan menghilangkan pemberitahuan ini.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Basis data telah diperbarui ke :applied, sementara kontainer ini masih menjalankan kode saat pertama dijalankan.",
|
||||
"This server is running version :running, but version :applied is installed": "Server ini menjalankan versi :running, tetapi versi yang terpasang adalah :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Untuk memperbarui, jalankan sudo ./update.sh di direktori instalasi — skrip ini bertanya sebelum mengunduh atau mengubah apa pun.",
|
||||
"To update, run this in the install directory:": "Untuk memperbarui, jalankan ini di direktori instalasi:",
|
||||
"Version :running is installed, but the update has not been run": "Versi :running terpasang, tetapi pembaruan belum dijalankan",
|
||||
"Updated ProjectSend to :to, from :from": "Memperbarui ProjectSend ke :to, dari :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend telah diperbarui ke versi baru",
|
||||
"Come and say hello": "Mampir dan sapa kami",
|
||||
"Continue to the dashboard": "Lanjutkan ke dasbor",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Gabung ke Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend punya Discord: kabar rilis, bantuan saat ada yang tidak beres, dan orang lain yang menjalankan perangkat lunak yang sama. Kami juga ada di sana.",
|
||||
"What's new": "Yang baru",
|
||||
"What each release brought to this installation.": "Apa yang dibawa setiap rilis ke instalasi ini.",
|
||||
"Add the rest of your team": "Tambahkan anggota tim lainnya",
|
||||
"Add your first client": "Tambahkan klien pertama Anda",
|
||||
"Check the scheduler is running": "Pastikan penjadwal berjalan",
|
||||
"Choose how your email looks": "Pilih tampilan email Anda",
|
||||
"Choose how your file lists look": "Pilih tampilan daftar berkas Anda",
|
||||
"Drop one in and choose who gets it.": "Jatuhkan satu dan pilih siapa yang menerimanya.",
|
||||
"Essential": "Penting",
|
||||
"Expiring files and queued email need it.": "Berkas kedaluwarsa dan email dalam antrean membutuhkannya.",
|
||||
"Four layouts for the client-facing pages.": "Empat tata letak untuk halaman yang dilihat klien.",
|
||||
"Getting started": "Memulai",
|
||||
"Group your clients": "Kelompokkan klien Anda",
|
||||
"Made with care in Argentina. Shared with the world.": "Dibuat dengan sepenuh hati di Argentina. Dibagikan ke seluruh dunia.",
|
||||
"Previewed on a real message.": "Dipratinjau pada pesan sungguhan.",
|
||||
"Resets and share links go out through it.": "Setel ulang kata sandi dan tautan berbagi lewat sana.",
|
||||
"Set up your mail server": "Siapkan server email Anda",
|
||||
"Share once instead of six times.": "Bagikan sekali, bukan enam kali.",
|
||||
"Staff accounts, each scoped by its role.": "Akun staf, masing-masing dibatasi oleh perannya.",
|
||||
"Thank you for installing ProjectSend": "Terima kasih telah memasang ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Terima kasih telah memperbarui ProjectSend",
|
||||
"The people you send files to.": "Orang-orang yang Anda kirimi berkas.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Versi singkat dari apa yang perlu dilakukan pada instalasi baru. Tidak ada yang kedaluwarsa di sini.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Anda kini di :version dan semuanya kembali berjalan. Terima kasih telah terus mempercayakan berbagi berkas Anda pada ProjectSend.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Anda kini di :version, dari :previous, dan semuanya kembali berjalan. Terima kasih telah terus mempercayakan berbagi berkas Anda pada ProjectSend.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Anda menjalankan versi :version, dan ini milik Anda. Inilah yang sebaiknya dilakukan lebih dulu.",
|
||||
"Thank you for choosing ProjectSend": "Terima kasih telah memilih ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Situs Anda sudah siap, dan menjaganya tetap berjalan adalah tugas kami. Inilah yang sebaiknya dilakukan lebih dulu.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 menyimpan semuanya. Notifikasi yang belum dibaca tidak pernah dihapus, seberapa pun lamanya.",
|
||||
":version is available": ":version tersedia",
|
||||
":version on :date": ":version pada :date",
|
||||
"Check now": "Periksa sekarang",
|
||||
"Checked a moment ago, so this is the answer from then.": "Pemeriksaan baru saja dilakukan, jadi ini jawaban dari saat itu.",
|
||||
"Checking…": "Memeriksa…",
|
||||
"Could not reach the update feed.": "Tidak dapat menghubungi umpan pembaruan.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Buat dump basis data sebelum menyentuh apa pun, dan berhenti jika dump gagal.",
|
||||
"Housekeeping": "Perawatan",
|
||||
"Keep failed jobs for": "Simpan tugas yang gagal selama",
|
||||
"Keep read notifications for": "Simpan notifikasi yang sudah dibaca selama",
|
||||
"Last checked :date": "Terakhir diperiksa: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Biarkan PHP dan worker apa adanya, untuk saat Anda memulai ulang sendiri.",
|
||||
"Other options": "Opsi lain",
|
||||
"Report which version is installed and which is available, and change nothing.": "Melaporkan versi yang terpasang dan versi yang tersedia, tanpa mengubah apa pun.",
|
||||
"Retention updated.": "Masa simpan diperbarui.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Dijalankan tanpa opsi, skrip akan menanyakan setiap hal ini sambil berjalan. Opsi ini untuk proses yang jawabannya sudah Anda ketahui.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Lewati pertanyaan tentang cadangan, untuk proses yang tidak Anda tunggui.",
|
||||
"The latest release (:tag) is not a version this can compare.": "Rilis terbaru (:tag) bukan versi yang bisa dibandingkan di sini.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Dua hal menumpuk dengan sendirinya. Inilah rentang waktu yang diikuti pembersihan harian di atas.",
|
||||
"Up to date": "Sudah terbaru",
|
||||
"Update checks are not available on this edition.": "Pemeriksaan pembaruan tidak tersedia pada edisi ini.",
|
||||
"Update options": "Opsi pembaruan",
|
||||
"Updated": "Diperbarui",
|
||||
"Version :version is available. You are running :current.": "Versi :version tersedia. Anda menggunakan :current.",
|
||||
"You are up to date, running version :current.": "Anda sudah menggunakan versi terbaru :current.",
|
||||
"days": "hari",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help menampilkan semua opsi, dan UPDATE.md menjelaskan seluruh prosedurnya, termasuk melakukannya secara manual.",
|
||||
"Fetch dashboard news": "Ambil kabar terbaru untuk dasbor",
|
||||
"Purge API request logs": "Bersihkan log permintaan API",
|
||||
"Purge erased accounts": "Bersihkan akun yang telah dihapus permanen",
|
||||
"Purge expired files": "Bersihkan berkas kedaluwarsa",
|
||||
"Purge failed jobs": "Bersihkan tugas yang gagal",
|
||||
"Purge orphan files": "Bersihkan berkas yatim",
|
||||
"Purge read notifications": "Bersihkan notifikasi yang sudah dibaca",
|
||||
"Purge stale chunked uploads": "Bersihkan unggahan bertahap yang terbengkalai",
|
||||
"Purge zip downloads": "Bersihkan unduhan ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Cocok dengan filter ini: :count, terbaru lebih dulu",
|
||||
"A file was previewed via the public group listing": "Sebuah berkas dipratinjau lewat daftar grup publik",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Siapa pun yang punya tautannya bisa membuka gambar, video, berkas audio, atau PDF di peramban alih-alih mengunduhnya. Matikan agar mengunduh menjadi satu-satunya cara melihat berkas publik.",
|
||||
"Clients can preview files": "Klien bisa mempratinjau berkas",
|
||||
"Downloads & previews": "Unduhan & pratinjau",
|
||||
"Downloads: :count": "Unduhan: :count",
|
||||
"Let visitors preview public files": "Izinkan pengunjung mempratinjau berkas publik",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Memungkinkan klien membuka gambar, video, berkas audio, atau PDF di portal, bukan sekadar mengunduhnya. Matikan agar mengunduh menjadi satu-satunya cara melihat berkas. Staf selalu bisa mempratinjau.",
|
||||
"No downloads match these filters.": "Tidak ada unduhan yang cocok dengan filter ini.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Belum ada yang mengunduh atau mempratinjau berkas ini.",
|
||||
"Preview :name": "Pratinjau :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Mempratinjau berkas \":subject\" lewat daftar grup publik",
|
||||
"Previews: :count": "Pratinjau: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Instalasi ini membangun image-nya sendiri, jadi menarik image tidak mengubah apa pun. Kalau rilis ini mengubah composer.lock atau frontend, jalankan juga composer install dan npm run build — prosedur lengkapnya ada di UPDATE.md.",
|
||||
"To update, run this in the directory you cloned:": "Untuk memperbarui, jalankan ini di direktori hasil klon:",
|
||||
"View all previews (:count)": "Lihat semua pratinjau (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Peramban Anda tidak bisa memutar berkas ini. Unduh berkasnya untuk melihat isinya."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count download",
|
||||
":count file deleted.|:count files deleted.": ":count file eliminato.|:count file eliminati.",
|
||||
":count file imported.|:count files imported.": ":count file importato.|:count file importati.",
|
||||
":count files": ":count file",
|
||||
":count folders": ":count cartelle",
|
||||
":count items have been shared with you": ":count elementi sono stati condivisi con te",
|
||||
":count members": ":count membri",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(e :count altro errore)",
|
||||
"(and :count more errors)": "(e :count altro errore)|(e :count altri errori)|(e :count altri errori)",
|
||||
"(deleted account)": "(account eliminato)",
|
||||
"0 means unlimited.": "0 significa illimitato.",
|
||||
"A client account request was approved": "Una richiesta di account cliente è stata approvata",
|
||||
"A client account request was denied": "Una richiesta di account cliente è stata rifiutata",
|
||||
"A client left a group": "Un cliente ha lasciato un gruppo",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "È stato creato un nuovo account cliente: :name (:email).",
|
||||
"A new client has registered": "Un nuovo cliente si è registrato",
|
||||
"A new ProjectSend version is available": "È disponibile una nuova versione di ProjectSend",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "La pagina di un gruppo pubblico è sempre raggiungibile dal suo link non appena lo contrassegni come pubblico — questa sezione controlla solo l'URL di base e un elenco facoltativo che mostra in una volta tutti i gruppi e i file pubblici.",
|
||||
"A public link was created": "Un link pubblico è stato creato",
|
||||
"A public link was revoked": "Un link pubblico è stato revocato",
|
||||
"A role was created": "Un ruolo è stato creato",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Amministrazione: richiesta di adesione a un gruppo",
|
||||
"Admin: new client registered": "Amministrazione: nuovo cliente registrato",
|
||||
"Administration": "Amministrazione",
|
||||
"After uploading you can assign it to clients and groups": "Dopo il caricamento potrai assegnarlo a clienti e gruppi",
|
||||
"Agree": "Essere d'accordo",
|
||||
"All": "Tutti",
|
||||
"All actions": "Tutte le azioni",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Tutti i commenti",
|
||||
"All failed jobs deleted.": "Tutte le operazioni fallite sono state eliminate.",
|
||||
"All files": "Tutti i file",
|
||||
"All groups": "Tutti i gruppi",
|
||||
"All permissions": "Tutte le autorizzazioni",
|
||||
"All rights reserved.": "Tutti i diritti riservati",
|
||||
"All roles": "Tutti i ruoli",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Chi lo utilizza non potrà più scaricare il file.",
|
||||
"Appearance": "Aspetto",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Si applica alle pagine pubbliche di gruppi e file rivolte ai visitatori e al portale clienti \"I miei file\".",
|
||||
"Apply filters": "Applica filtri",
|
||||
"Approve": "Approva",
|
||||
"Approve account requests": "Approvare le richieste di account",
|
||||
"Approve group membership requests": "Approvare le richieste di adesione ai gruppi",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Vuoi davvero eliminare il tuo account?",
|
||||
"Assign": "Assegnare",
|
||||
"Assign categories to files": "Assegnare categorie ai file",
|
||||
"Assigned": "Assegnato",
|
||||
"Assigned clients": "Clienti assegnati",
|
||||
"Assigned the file \":subject\" to :target": "Ha assegnato il file \":subject\" a :target",
|
||||
"Associate": "Associare",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Autore",
|
||||
"Available groups": "Gruppi disponibili",
|
||||
"Available placeholders": "Segnaposto disponibili",
|
||||
"Back to My files": "Torna a I miei file",
|
||||
"Bad Gateway": "Gateway cattivo",
|
||||
"Bad Request": "Cattiva richiesta",
|
||||
"Bandwidth Limit Exceeded": "Limita di banda superato",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Corpo del messaggio",
|
||||
"Browse": "Naviga",
|
||||
"Browse publicly shared groups and files below.": "Sfoglia qui sotto i gruppi e i file condivisi pubblicamente.",
|
||||
"Built-in": "Predefinito",
|
||||
"Built-in role names cannot be changed.": "Il nome dei ruoli predefiniti non può essere modificato.",
|
||||
"Built-in roles cannot be deleted.": "I ruoli predefiniti non possono essere eliminati.",
|
||||
"Call to action": "Invito all'azione",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Conferma password",
|
||||
"Conflict": "Conflitto",
|
||||
"Connect": "Collega",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Collega un bucket esterno — compatibile con S3 (AWS S3, MinIO, Backblaze) o Google Cloud Storage — come spazio di archiviazione per i nuovi caricamenti.",
|
||||
"Connection Closed Without Response": "Connessione chiusa senza risposta",
|
||||
"Connection Timed Out": "Tempo scaduto per la connessione",
|
||||
"Continue": "Continua",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Disattiva subito il tuo account; verrà cancellato definitivamente dopo :days giorni",
|
||||
"Deactivated the account \":subject\"": "Ha disattivato l'account \":subject\"",
|
||||
"Default": "Predefinito",
|
||||
"Defaults to the file name.": "Per impostazione predefinita coincide con il nome del file.",
|
||||
"Delete": "Elimina",
|
||||
"Delete :name": "Cancella :name",
|
||||
"Delete account": "Elimina account",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Rifiuta",
|
||||
"Deny this request?": "Rifiutare questa richiesta?",
|
||||
"Description": "Descrizione",
|
||||
"Description (optional)": "Descrizione (facoltativa)",
|
||||
"Detach": "Scollega",
|
||||
"Details": "Dettagli",
|
||||
"Disable": "Disabilita",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Vai alla pagina :page",
|
||||
"Go to the login form": "Vai al modulo di accesso",
|
||||
"Gone": "Andata",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Grigio",
|
||||
"Green": "Verde",
|
||||
"Group": "Gruppo",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Accedi",
|
||||
"Logout": "Esci",
|
||||
"Loop Detected": "Loop identificato",
|
||||
"Mail provider settings saved.": "Impostazioni del provider e-mail salvate.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "In manutenzione",
|
||||
"Manage custom fields": "Gestire i campi personalizzati",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Non è ancora stato condiviso alcun file con te.",
|
||||
"No files here yet.": "Ancora nessun file qui.",
|
||||
"No files or folders match your search.": "Nessun file o cartella corrisponde alla tua ricerca.",
|
||||
"No files yet. Upload the first one.": "Ancora nessun file. Carica il primo.",
|
||||
"No folder": "Nessuna cartella",
|
||||
"No groups match these filters.": "Nessun gruppo corrisponde a questi filtri.",
|
||||
"No groups yet.": "Ancora nessun gruppo.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Ancora nessun destinatario aggiunto.",
|
||||
"No requests match your search.": "Nessuna richiesta corrisponde alla tua ricerca.",
|
||||
"No roles match this filter.": "Nessun ruolo corrisponde a questo filtro.",
|
||||
"No service account key has been saved yet.": "Non è ancora stata salvata nessuna chiave dell'account di servizio.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Nessun limite di dimensione. I caricamenti possono essere messi in pausa e riprendono automaticamente dopo un'interruzione.",
|
||||
"No users match these filters.": "Nessun utente corrisponde a questi filtri.",
|
||||
"No users yet.": "Ancora nessun utente.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Arancione",
|
||||
"Origin Is Unreachable": "Origine non raggiungibile",
|
||||
"Outgoing email": "E-mail in uscita",
|
||||
"Outgoing email theme": "Tema delle e-mail in uscita",
|
||||
"Page :current of :last": "Pagina :current di :last",
|
||||
"Page Expired": "Pagina scaduta",
|
||||
"Pagination Navigation": "Navigazione della Paginazione",
|
||||
"Partial Content": "Contenuto parziale",
|
||||
"Password": "Password",
|
||||
"Password reset": "Reimpostazione della password",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Incolla il file di chiave JSON di un account di servizio con accesso in lettura e scrittura agli oggetti del bucket. Viene salvato cifrato e non viene più mostrato.",
|
||||
"Path": "Percorso",
|
||||
"Payload Too Large": "Payload troppo grande",
|
||||
"Payment Required": "Pagamento richiesto",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Impostazioni dell'elenco pubblico",
|
||||
"Public listing URL": "URL dell'elenco pubblico",
|
||||
"Public pages & portal": "Pagine pubbliche e portale",
|
||||
"Public pages and client portal theme": "Tema delle pagine pubbliche e del portale clienti",
|
||||
"Published": "Pubblicato",
|
||||
"Purple": "Viola",
|
||||
"Quota": "Quota",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Ripristina",
|
||||
"Restore :name": "Ripristina :name",
|
||||
"Restricted extension": "Estensione limitata",
|
||||
"Result": "Risultato",
|
||||
"results": "risultati",
|
||||
"Review membership requests": "Esaminare le richieste di adesione",
|
||||
"Review pending requests": "Esamina le richieste in sospeso",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Nome del ruolo",
|
||||
"Role updated.": "Ruolo aggiornato.",
|
||||
"Roles": "Ruoli",
|
||||
"S3-compatible": "Compatibile con S3",
|
||||
"Sample email": "E-mail di esempio",
|
||||
"Save": "Salva",
|
||||
"Save :name": "Salva :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Invia subito una vera e-mail, saltando la coda, così puoi verificare che le impostazioni qui sopra funzionino davvero.",
|
||||
"Separate paragraphs with a blank line.": "Separa i paragrafi con una riga vuota.",
|
||||
"Server Error": "Errore server",
|
||||
"Service account key": "Chiave dell'account di servizio",
|
||||
"Service Unavailable": "Servizio non disponibile",
|
||||
"Session Has Expired": "Sessione scaduta",
|
||||
"Set file expiration dates": "Impostare le date di scadenza dei file",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Impostazioni",
|
||||
"Settings that apply to the whole installation": "Impostazioni valide per l'intera installazione",
|
||||
"Setup complete": "Configurazione completata",
|
||||
"Share a file with the people who manage your account.": "Condividi un file con le persone che gestiscono il tuo account.",
|
||||
"Share files with several clients at once": "Condividi file con più clienti in una volta sola",
|
||||
"Shared the folder \":subject\" with :target": "Ha condiviso la cartella \":subject\" con :target",
|
||||
"Shared with": "Condiviso con",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Scambiando protocolli",
|
||||
"System": "Sistema",
|
||||
"System Administrator": "Amministratore di sistema",
|
||||
"System settings": "Impostazioni di sistema",
|
||||
"System settings were updated": "Le impostazioni di sistema sono state aggiornate",
|
||||
"System users": "Utenti di sistema",
|
||||
"Tag": "Etichetta",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Prova",
|
||||
"Text": "Testo",
|
||||
"Text or author": "Testo o autore",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Questo non sembra un file di chiave dell'account di servizio: non è JSON valido.",
|
||||
"That service account key file is missing its :field.": "A questo file di chiave dell'account di servizio manca :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "L'account di :name verrà eliminato definitivamente. L'operazione non è reversibile.",
|
||||
"The account password was changed": "La password dell'account è stata modificata",
|
||||
"The account request of :name will be denied and the account deleted.": "La richiesta di account di :name verrà rifiutata e l'account eliminato.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "I gruppi a cui appartieni e quelli a cui puoi chiedere di aderire",
|
||||
"The header/footer style used for every outgoing notification email.": "Lo stile di intestazione e piè di pagina usato per ogni e-mail di notifica in uscita.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "L'aspetto delle pagine pubbliche per i visitatori, del portale clienti e delle e-mail di notifica in uscita.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Il server di posta vero e proprio si configura nel file .env di questa installazione (variabili MAIL_*), non qui.",
|
||||
"The people who administer this installation and upload files": "Le persone che amministrano questa installazione e caricano i file",
|
||||
"The people you share files with": "Le persone con cui condividi i file",
|
||||
"The provided two-factor authentication code was invalid.": "Il codice di autenticazione a due fattori inserito non è valido.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Non c'è ancora nulla da configurare.",
|
||||
"Test email from :site": "E-mail di prova da :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Questi file verranno rimossi definitivamente dal disco. L'operazione non è reversibile.",
|
||||
"Theme": "Tema",
|
||||
"Theme updated.": "Tema aggiornato.",
|
||||
"Theming": "Temi",
|
||||
"Theming settings": "Impostazioni dei temi",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Questo caricamento supererebbe la tua quota di spazio di :quota MB.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Suggerimento: trascina file e cartelle su una cartella o su un percorso di navigazione per spostarli.",
|
||||
"Title": "Titolo",
|
||||
"Title (optional)": "Titolo (facoltativo)",
|
||||
"TLS": "TLS",
|
||||
"to": "a",
|
||||
"To": "A",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Troppi file selezionati. Riduci la selezione e riprova.",
|
||||
"Too Many Requests": "Troppe richieste",
|
||||
"Transactional email settings for this installation": "Impostazioni delle e-mail transazionali di questa installazione",
|
||||
"Transfers — last 30 days": "Trasferimenti — ultimi 30 giorni",
|
||||
"Translate": "Traduci",
|
||||
"Translate It": "Traducilo",
|
||||
"Turns all transactional email for this installation on or off.": "Attiva o disattiva tutte le e-mail transazionali di questa installazione.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Ha caricato il file \":subject\"",
|
||||
"Uploader": "Autore del caricamento",
|
||||
"Uploads": "Caricamenti",
|
||||
"Uploads (chart)": "Caricamenti (grafico)",
|
||||
"URI Too Long": "URI troppo lungo",
|
||||
"URL slug": "Slug dell'URL",
|
||||
"Use a custom link instead of a random one": "Usa un link personalizzato al posto di uno casuale",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Questa è l'unica volta in cui verrà mostrato. Ne conserviamo solo un hash, quindi non potrà essere recuperato in seguito.",
|
||||
"Copy to clipboard": "Copia negli appunti",
|
||||
"Copied": "Copiato",
|
||||
"Your tokens": "I tuoi token",
|
||||
"Revoking a token takes effect immediately.": "La revoca di un token ha effetto immediato.",
|
||||
"You have not created any tokens yet.": "Non hai ancora creato alcun token.",
|
||||
"Expired": "Scaduto",
|
||||
"Never used": "Mai usato",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Gestire i campi personalizzati dei clienti",
|
||||
"Moderate comments": "Moderare i commenti",
|
||||
"Only me": "Solo io",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Visibile a chiunque possa aprire questo file, compresi i visitatori non connessi.",
|
||||
"Anyone, including visitors who are not logged in": "Chiunque, compresi i visitatori non connessi",
|
||||
"Staff and clients": "Staff e clienti",
|
||||
"Staff only": "Solo staff",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "I commenti già scritti restano leggibili anche se un file smette di accettarne di nuovi.",
|
||||
"Comments awaiting approval": "Commenti in attesa di approvazione",
|
||||
"Comment settings": "Impostazioni dei commenti",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Commenti lasciati da visitatori non connessi. Nessuno può vederli finché non li approvi.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Collega un bucket esterno compatibile con S3 (AWS S3, MinIO o un altro servizio compatibile) come spazio di archiviazione per i nuovi caricamenti.",
|
||||
"Custom endpoint": "Endpoint personalizzato",
|
||||
"Custom field created.": "Campo personalizzato creato.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Nessuna operazione fallita.",
|
||||
"No files match these filters.": "Nessun file corrisponde a questi filtri.",
|
||||
"None of the selected files could be edited.": "Nessuno dei file selezionati ha potuto essere modificato.",
|
||||
"Nothing is waiting for approval.": "Non c'è nulla in attesa di approvazione.",
|
||||
"of :count": "di :count",
|
||||
"of :quota": "di :quota",
|
||||
"on": "il",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Pagina",
|
||||
"Path prefix": "Prefisso del percorso",
|
||||
"Pending": "In sospeso",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Controlla periodicamente se il repository pubblico di projectsend.org offre una versione più recente e mostra un avviso nel pannello. Non viene mai scaricato né applicato nulla automaticamente.",
|
||||
"Permanently failed": "Fallita definitivamente",
|
||||
"Post comment": "Pubblica commento",
|
||||
"Preview of :name": "Anteprima di :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Rimuovi l'autenticazione a due fattori",
|
||||
"Remove two-factor authentication?": "Rimuovere l'autenticazione a due fattori?",
|
||||
"Require a verified email address": "Richiedi un indirizzo email verificato",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Esegui questi comandi come l'utente con cui gira il tuo server web. INSTALL.md contiene la procedura completa.",
|
||||
"Score threshold": "Soglia del punteggio",
|
||||
"Search timezones…": "Cerca fusi orari…",
|
||||
"Security check failed. Please try again.": "Il controllo di sicurezza non è riuscito. Riprova.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Questo sito è protetto da reCAPTCHA e si applicano l'Informativa sulla privacy e i Termini di servizio di Google.",
|
||||
"Those files have reached their download limit.": "Quei file hanno raggiunto il loro limite di download.",
|
||||
"Timezone": "Fuso orario",
|
||||
"To update, back up your database and files, then:": "Per aggiornare, fai un backup del database e dei file, poi:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Per aggiornare, segui i passaggi in INSTALL.md: prima fai un backup, poi estrai la release ed esegui le migrazioni.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Disattiva questa opzione solo se questo server non deve fare richieste in uscita. Se il servizio non è raggiungibile la password viene comunque accettata, quindi un'installazione offline non viene mai bloccata da essa.",
|
||||
"Two-factor authentication removed.": "Autenticazione a due fattori rimossa.",
|
||||
"Two-factor authentication removed by an admin": "Autenticazione a due fattori rimossa da un amministratore",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Il tuo account è impostato su :timezone, quindi questo non cambierà come vedi tu le date.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Il tuo tenant, non \"common\". Microsoft permette a un utente di cambiare l'indirizzo email del proprio account, quindi un accesso è affidabile solo quando il token proviene dal tenant che hai indicato qui.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Il tuo provider ha rifiutato la chiave segreta di questa installazione, quindi i moduli protetti vengono lasciati passare senza controllo. Controlla la chiave segreta qui sotto.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "I tuoi moduli pubblici sono protetti. Puoi passare alle tue chiavi in qualsiasi momento."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "I tuoi moduli pubblici sono protetti. Puoi passare alle tue chiavi in qualsiasi momento.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Se sei tornato indietro di proposito, esegui php artisan projectsend:update in modo che il database corrisponda al codice che hai ripristinato.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Chiede conferma prima di cercare una versione, prima di scaricarla e prima di toccare la tua installazione, e verifica il checksum di ciò che scarica. UPDATE.md contiene la procedura completa.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "I nuovi file sono al loro posto, ma il database è stato portato solo alla :applied. Finché non esegui l'aggiornamento, questa installazione gira con codice che non corrisponde al suo database.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Controlla periodicamente se il repository pubblico di projectsend.org offre una versione più recente e mostra un avviso nel pannello. Non viene mai scaricato né applicato nulla automaticamente: aggiornare è sempre qualcosa che avvii tu, e lo script di aggiornamento chiede conferma prima di ogni passaggio.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Il database è stato aggiornato alla :applied, e PHP continua a servire il codice che aveva compilato prima. Ricaricandolo questo avviso sparisce.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Il database è stato aggiornato alla :applied, e questo container sta ancora eseguendo il codice con cui è partito.",
|
||||
"This server is running version :running, but version :applied is installed": "Questo server sta eseguendo la versione :running, ma la versione installata è :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Per aggiornare, esegui sudo ./update.sh nella cartella di installazione: chiede conferma prima di scaricare o cambiare qualcosa.",
|
||||
"To update, run this in the install directory:": "Per aggiornare, esegui questo nella cartella di installazione:",
|
||||
"Version :running is installed, but the update has not been run": "La versione :running è installata, ma l'aggiornamento non è stato eseguito",
|
||||
"Updated ProjectSend to :to, from :from": "Ha aggiornato ProjectSend a :to, da :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend è stato aggiornato a una nuova versione",
|
||||
"Come and say hello": "Vieni a salutarci",
|
||||
"Continue to the dashboard": "Continua al pannello",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Entra nel Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend ha un Discord: novità sulle versioni, aiuto quando qualcosa non si comporta come dovrebbe e altre persone che usano lo stesso software. Ci siamo anche noi.",
|
||||
"What's new": "Novità",
|
||||
"What each release brought to this installation.": "Cosa ha portato ogni versione a questa installazione.",
|
||||
"Add the rest of your team": "Aggiungi il resto del tuo staff",
|
||||
"Add your first client": "Aggiungi il tuo primo cliente",
|
||||
"Check the scheduler is running": "Controlla che il pianificatore sia attivo",
|
||||
"Choose how your email looks": "Scegli l'aspetto delle tue e-mail",
|
||||
"Choose how your file lists look": "Scegli l'aspetto dei tuoi elenchi di file",
|
||||
"Drop one in and choose who gets it.": "Caricane uno e scegli chi lo riceve.",
|
||||
"Essential": "Indispensabile",
|
||||
"Expiring files and queued email need it.": "I file in scadenza e le e-mail in coda dipendono da lui.",
|
||||
"Four layouts for the client-facing pages.": "Quattro layout per le pagine che vedono i clienti.",
|
||||
"Getting started": "Primi passi",
|
||||
"Group your clients": "Raggruppa i tuoi clienti",
|
||||
"Made with care in Argentina. Shared with the world.": "Fatto con cura in Argentina. Condiviso con il mondo.",
|
||||
"Previewed on a real message.": "In anteprima su un messaggio reale.",
|
||||
"Resets and share links go out through it.": "Reimpostazioni e link di condivisione passano di lì.",
|
||||
"Set up your mail server": "Configura il tuo server di posta",
|
||||
"Share once instead of six times.": "Condividi una volta invece di sei.",
|
||||
"Staff accounts, each scoped by its role.": "Account dello staff, ognuno limitato dal suo ruolo.",
|
||||
"Thank you for installing ProjectSend": "Grazie per aver installato ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Grazie per aver aggiornato ProjectSend",
|
||||
"The people you send files to.": "Le persone a cui mandi i file.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "La versione breve di cosa fare su una nuova installazione. Niente di tutto questo scade.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Ora sei alla :version e tutto è tornato attivo. Grazie per continuare ad affidare i tuoi file a ProjectSend.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Ora sei alla :version, dalla :previous, e tutto è tornato attivo. Grazie per continuare ad affidare i tuoi file a ProjectSend.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Stai usando la versione :version, ed è tua. Ecco cosa conviene fare per primo.",
|
||||
"Thank you for choosing ProjectSend": "Grazie per aver scelto ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Il tuo sito è pronto, e tenerlo in funzione è compito nostro. Ecco cosa conviene fare per primo.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 conserva tutto. Le notifiche non lette non vengono mai eliminate, per quanto vecchie siano.",
|
||||
":version is available": ":version è disponibile",
|
||||
":version on :date": ":version il :date",
|
||||
"Check now": "Controlla ora",
|
||||
"Checked a moment ago, so this is the answer from then.": "Il controllo è di un momento fa, quindi questa è la risposta di allora.",
|
||||
"Checking…": "Controllo in corso…",
|
||||
"Could not reach the update feed.": "Impossibile raggiungere il feed degli aggiornamenti.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Fai un dump del database prima di toccare qualsiasi cosa, e fermati se il dump fallisce.",
|
||||
"Housekeeping": "Manutenzione",
|
||||
"Keep failed jobs for": "Conserva le operazioni fallite per",
|
||||
"Keep read notifications for": "Conserva le notifiche lette per",
|
||||
"Last checked :date": "Ultimo controllo: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Non toccare PHP né il worker, per quando li riavvii tu.",
|
||||
"Other options": "Altre opzioni",
|
||||
"Report which version is installed and which is available, and change nothing.": "Riporta quale versione è installata e quale è disponibile, senza cambiare nulla.",
|
||||
"Retention updated.": "Conservazione aggiornata.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Eseguito senza opzioni, lo script chiede ognuna di queste cose strada facendo. Queste servono per le esecuzioni in cui conosci già la risposta.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Salta la domanda sui backup, per un'esecuzione che non stai seguendo.",
|
||||
"The latest release (:tag) is not a version this can compare.": "L'ultima versione pubblicata (:tag) non è una versione confrontabile qui.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Due cose si accumulano da sole. Questi sono i tempi a cui si attengono le pulizie quotidiane qui sopra.",
|
||||
"Up to date": "Aggiornato",
|
||||
"Update checks are not available on this edition.": "I controlli degli aggiornamenti non sono disponibili in questa edizione.",
|
||||
"Update options": "Opzioni di aggiornamento",
|
||||
"Updated": "Ultimo aggiornamento",
|
||||
"Version :version is available. You are running :current.": "La versione :version è disponibile. Stai usando la :current.",
|
||||
"You are up to date, running version :current.": "Sei aggiornato, con la versione :current.",
|
||||
"days": "giorni",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help elenca tutte le opzioni, e UPDATE.md descrive l'intera procedura, compreso farla a mano.",
|
||||
"Fetch dashboard news": "Recupera le novità della bacheca",
|
||||
"Purge API request logs": "Elimina il registro delle richieste API",
|
||||
"Purge erased accounts": "Elimina gli account cancellati",
|
||||
"Purge expired files": "Elimina i file scaduti",
|
||||
"Purge failed jobs": "Elimina le operazioni fallite",
|
||||
"Purge orphan files": "Elimina i file orfani",
|
||||
"Purge read notifications": "Elimina le notifiche lette",
|
||||
"Purge stale chunked uploads": "Elimina i caricamenti a blocchi abbandonati",
|
||||
"Purge zip downloads": "Elimina i download ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Corrispondenze con questo filtro: :count, dalla più recente",
|
||||
"A file was previewed via the public group listing": "Un file è stato visualizzato in anteprima tramite l'elenco pubblico dei gruppi",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Chiunque abbia il link può aprire un'immagine, un video, un file audio o un PDF nel browser invece di scaricarlo. Disattivalo se vuoi che il download sia l'unico modo di vedere un file pubblico.",
|
||||
"Clients can preview files": "I clienti possono visualizzare i file in anteprima",
|
||||
"Downloads & previews": "Download e anteprime",
|
||||
"Downloads: :count": "Download: :count",
|
||||
"Let visitors preview public files": "Consenti ai visitatori di vedere l'anteprima dei file pubblici",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Consente ai clienti di aprire un'immagine, un video, un file audio o un PDF nel portale invece di doverlo solo scaricare. Disattivalo se vuoi che il download sia l'unico modo di vedere un file. Lo staff può sempre usare l'anteprima.",
|
||||
"No downloads match these filters.": "Nessun download corrisponde a questi filtri.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Nessuno ha ancora scaricato o visualizzato in anteprima questo file.",
|
||||
"Preview :name": "Anteprima di :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Ha visualizzato in anteprima il file \":subject\" tramite l'elenco pubblico dei gruppi",
|
||||
"Previews: :count": "Anteprime: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Questa installazione costruisce le proprie immagini, quindi scaricarne una non cambia nulla. Se la versione ha modificato composer.lock o il frontend, esegui anche composer install e npm run build: la procedura completa è in UPDATE.md.",
|
||||
"To update, run this in the directory you cloned:": "Per aggiornare, esegui questo nella cartella che hai clonato:",
|
||||
"View all previews (:count)": "Vedi tutte le anteprime (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Il tuo browser non può riprodurre questo file. Scaricalo per vederlo."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count 回のダウンロード",
|
||||
":count file deleted.|:count files deleted.": ":count 件のファイルを削除しました。|:count 件のファイルを削除しました。",
|
||||
":count file imported.|:count files imported.": ":count 件のファイルをインポートしました。|:count 件のファイルをインポートしました。",
|
||||
":count files": ":count 件のファイル",
|
||||
":count folders": ":count 件のフォルダ",
|
||||
":count items have been shared with you": ":count 件のアイテムが共有されました",
|
||||
":count members": ":count 人のメンバー",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(その他、:countエラーあり)",
|
||||
"(and :count more errors)": "(その他、:countエラーあり)",
|
||||
"(deleted account)": "(削除済みのアカウント)",
|
||||
"0 means unlimited.": "0 は無制限を意味します。",
|
||||
"A client account request was approved": "クライアントアカウントの申請が承認されました",
|
||||
"A client account request was denied": "クライアントアカウントの申請が却下されました",
|
||||
"A client left a group": "クライアントがグループを退出しました",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "新しいクライアントアカウントが作成されました: :name (:email)。",
|
||||
"A new client has registered": "新しいクライアントが登録しました",
|
||||
"A new ProjectSend version is available": "ProjectSend の新しいバージョンが利用できます",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "グループを公開に設定すると、そのページには常に専用リンクからアクセスできます。ここで設定するのは基準となる URL と、公開中のグループとファイルを一度に一覧表示する任意のディレクトリだけです。",
|
||||
"A public link was created": "公開リンクが作成されました",
|
||||
"A public link was revoked": "公開リンクが無効化されました",
|
||||
"A role was created": "ロールが作成されました",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "管理者向け: グループ参加の申請",
|
||||
"Admin: new client registered": "管理者向け: 新しいクライアントが登録",
|
||||
"Administration": "管理",
|
||||
"After uploading you can assign it to clients and groups": "アップロード後に、クライアントやグループへ割り当てられます",
|
||||
"Agree": "同意",
|
||||
"All": "すべて",
|
||||
"All actions": "すべての操作",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "すべてのコメント",
|
||||
"All failed jobs deleted.": "失敗したジョブをすべて削除しました。",
|
||||
"All files": "すべてのファイル",
|
||||
"All groups": "すべてのグループ",
|
||||
"All permissions": "すべての権限",
|
||||
"All rights reserved.": "All rights reserved.",
|
||||
"All roles": "すべてのロール",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "このリンクを使っている人は、ファイルをダウンロードできなくなります。",
|
||||
"Appearance": "外観",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "訪問者向けの公開グループ・ファイルページと、クライアントポータル「マイファイル」に適用されます。",
|
||||
"Apply filters": "フィルタを適用",
|
||||
"Approve": "承認",
|
||||
"Approve account requests": "アカウント申請の承認",
|
||||
"Approve group membership requests": "グループ参加申請の承認",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "本当にアカウントを削除しますか?",
|
||||
"Assign": "割当",
|
||||
"Assign categories to files": "ファイルへのカテゴリ割り当て",
|
||||
"Assigned": "割り当て済み",
|
||||
"Assigned clients": "担当クライアント",
|
||||
"Assigned the file \":subject\" to :target": "ファイル「:subject」を :target に割り当てました",
|
||||
"Associate": "仲間",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "作成者",
|
||||
"Available groups": "参加できるグループ",
|
||||
"Available placeholders": "使用できるプレースホルダー",
|
||||
"Back to My files": "マイファイルに戻る",
|
||||
"Bad Gateway": "不正なゲートウェイ",
|
||||
"Bad Request": "不正なリクエスト",
|
||||
"Bandwidth Limit Exceeded": "帯域幅の制限を超過",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "本文",
|
||||
"Browse": "ブラウズ",
|
||||
"Browse publicly shared groups and files below.": "公開共有されているグループとファイルを以下からご覧ください。",
|
||||
"Built-in": "組み込み",
|
||||
"Built-in role names cannot be changed.": "組み込みロールの名前は変更できません。",
|
||||
"Built-in roles cannot be deleted.": "組み込みロールは削除できません。",
|
||||
"Call to action": "行動を促すボタン",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "パスワードの確認",
|
||||
"Conflict": "競合",
|
||||
"Connect": "接続",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "新しいアップロードの保存先として、外部のバケット (S3 互換 — AWS S3、MinIO、Backblaze — または Google Cloud Storage) を接続します。",
|
||||
"Connection Closed Without Response": "応答なしで接続が閉じられました",
|
||||
"Connection Timed Out": "接続がタイムアウト",
|
||||
"Continue": "継続",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "アカウントを今すぐ無効化します。:days 日後に完全に消去されます",
|
||||
"Deactivated the account \":subject\"": "アカウント「:subject」を無効化しました",
|
||||
"Default": "既定",
|
||||
"Defaults to the file name.": "既定ではファイル名が使われます。",
|
||||
"Delete": "削除",
|
||||
"Delete :name": ":nameを削除",
|
||||
"Delete account": "アカウントを削除",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "却下",
|
||||
"Deny this request?": "この申請を却下しますか?",
|
||||
"Description": "説明",
|
||||
"Description (optional)": "説明 (任意)",
|
||||
"Detach": "解除",
|
||||
"Details": "詳細",
|
||||
"Disable": "無効化",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": ":pageページへ",
|
||||
"Go to the login form": "ログインフォームへ",
|
||||
"Gone": "消滅",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "グレー",
|
||||
"Green": "緑",
|
||||
"Group": "グループ",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "ログイン",
|
||||
"Logout": "ログアウト",
|
||||
"Loop Detected": "ループを検出",
|
||||
"Mail provider settings saved.": "メールプロバイダーの設定を保存しました。",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "メンテナンス中",
|
||||
"Manage custom fields": "カスタムフィールドの管理",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "まだファイルは共有されていません。",
|
||||
"No files here yet.": "ここにはまだファイルがありません。",
|
||||
"No files or folders match your search.": "検索条件に一致するファイルやフォルダはありません。",
|
||||
"No files yet. Upload the first one.": "ファイルはまだありません。最初の 1 件をアップロードしましょう。",
|
||||
"No folder": "フォルダなし",
|
||||
"No groups match these filters.": "この条件に一致するグループはありません。",
|
||||
"No groups yet.": "グループはまだありません。",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "宛先はまだ追加されていません。",
|
||||
"No requests match your search.": "検索条件に一致する申請はありません。",
|
||||
"No roles match this filter.": "この条件に一致するロールはありません。",
|
||||
"No service account key has been saved yet.": "サービスアカウントキーはまだ保存されていません。",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "サイズ制限はありません。アップロードは一時停止でき、中断後は自動的に再開します。",
|
||||
"No users match these filters.": "この条件に一致するユーザーはいません。",
|
||||
"No users yet.": "ユーザーはまだいません。",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "オレンジ",
|
||||
"Origin Is Unreachable": "オリジンに到達できません",
|
||||
"Outgoing email": "送信メール",
|
||||
"Outgoing email theme": "送信メールのテーマ",
|
||||
"Page :current of :last": ":last ページ中 :current ページ",
|
||||
"Page Expired": "ページが無効です",
|
||||
"Pagination Navigation": "ページネーション",
|
||||
"Partial Content": "部分的なコンテンツ",
|
||||
"Password": "パスワード",
|
||||
"Password reset": "パスワードの再設定",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "バケット内のオブジェクトへの読み取りと書き込み権限を持つサービスアカウントの JSON キーファイルを貼り付けてください。暗号化して保存され、二度と表示されません。",
|
||||
"Path": "パス",
|
||||
"Payload Too Large": "ペイロードが大きすぎます",
|
||||
"Payment Required": "お支払いが必要",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "公開一覧の設定",
|
||||
"Public listing URL": "公開一覧の URL",
|
||||
"Public pages & portal": "公開ページとポータル",
|
||||
"Public pages and client portal theme": "公開ページとクライアントポータルのテーマ",
|
||||
"Published": "公開日",
|
||||
"Purple": "紫",
|
||||
"Quota": "上限",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "復元",
|
||||
"Restore :name": ":nameを復元",
|
||||
"Restricted extension": "制限された拡張子",
|
||||
"Result": "結果",
|
||||
"results": "結果",
|
||||
"Review membership requests": "参加申請の確認",
|
||||
"Review pending requests": "保留中の申請を確認",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "ロール名",
|
||||
"Role updated.": "ロールを更新しました。",
|
||||
"Roles": "ロール",
|
||||
"S3-compatible": "S3 互換",
|
||||
"Sample email": "サンプルメール",
|
||||
"Save": "保存",
|
||||
"Save :name": ":nameを保存",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "キューを通さず今すぐ実際のメールを送信し、上の設定が本当に機能するか確認できます。",
|
||||
"Separate paragraphs with a blank line.": "段落は空行で区切ってください。",
|
||||
"Server Error": "サーバーエラー",
|
||||
"Service account key": "サービスアカウントキー",
|
||||
"Service Unavailable": "サービスは利用できません",
|
||||
"Session Has Expired": "セッションの有効期限切れ",
|
||||
"Set file expiration dates": "ファイルの有効期限の設定",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "設定",
|
||||
"Settings that apply to the whole installation": "インストール全体に適用される設定",
|
||||
"Setup complete": "設定が完了しました",
|
||||
"Share a file with the people who manage your account.": "アカウントを管理している担当者にファイルを共有します。",
|
||||
"Share files with several clients at once": "複数のクライアントにまとめてファイルを共有できます",
|
||||
"Shared the folder \":subject\" with :target": "フォルダ「:subject」を :target に共有しました",
|
||||
"Shared with": "共有先",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "プロトコルの切替",
|
||||
"System": "システム",
|
||||
"System Administrator": "システム管理者",
|
||||
"System settings": "システム設定",
|
||||
"System settings were updated": "システム設定が更新されました",
|
||||
"System users": "システムユーザー",
|
||||
"Tag": "タグ",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "テスト",
|
||||
"Text": "テキスト",
|
||||
"Text or author": "本文または投稿者",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "サービスアカウントのキーファイルではないようです。有効な JSON ではありません。",
|
||||
"That service account key file is missing its :field.": "このサービスアカウントキーファイルには :field がありません。",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": ":name のアカウントを完全に削除します。この操作は取り消せません。",
|
||||
"The account password was changed": "アカウントのパスワードが変更されました",
|
||||
"The account request of :name will be denied and the account deleted.": ":name のアカウント申請を却下し、アカウントを削除します。",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "所属しているグループと、参加を申請できるグループ",
|
||||
"The header/footer style used for every outgoing notification email.": "送信するすべての通知メールで使われるヘッダー・フッターのスタイルです。",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "訪問者向け公開ページ、クライアントポータル、送信する通知メールの見た目です。",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "メールサーバー自体は、ここではなくこのインストールの .env ファイル (MAIL_* 変数) で設定します。",
|
||||
"The people who administer this installation and upload files": "このインストールを管理し、ファイルをアップロードする人たち",
|
||||
"The people you share files with": "ファイルを共有する相手",
|
||||
"The provided two-factor authentication code was invalid.": "入力された二要素認証コードが正しくありません。",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "設定できる項目はまだありません。",
|
||||
"Test email from :site": ":site からのテストメール",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "これらのファイルをディスクから完全に削除します。この操作は取り消せません。",
|
||||
"Theme": "テーマ",
|
||||
"Theme updated.": "テーマを更新しました。",
|
||||
"Theming": "テーマ",
|
||||
"Theming settings": "テーマ設定",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "このアップロードは、ストレージ上限 :quota MB を超えてしまいます。",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "ヒント: ファイルやフォルダをフォルダやパンくずリストにドラッグすると移動できます。",
|
||||
"Title": "タイトル",
|
||||
"Title (optional)": "タイトル (任意)",
|
||||
"TLS": "TLS",
|
||||
"to": "に",
|
||||
"To": "宛先",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "選択したファイルが多すぎます。数を減らしてもう一度お試しください。",
|
||||
"Too Many Requests": "リクエストが多すぎます",
|
||||
"Transactional email settings for this installation": "このインストールの各種メール設定",
|
||||
"Transfers — last 30 days": "転送 — 過去 30 日間",
|
||||
"Translate": "翻訳",
|
||||
"Translate It": "翻訳してください",
|
||||
"Turns all transactional email for this installation on or off.": "このインストールの各種メール送信をまとめて有効・無効にします。",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "ファイル「:subject」をアップロードしました",
|
||||
"Uploader": "アップロード者",
|
||||
"Uploads": "アップロード",
|
||||
"Uploads (chart)": "アップロード (グラフ)",
|
||||
"URI Too Long": "URLが長すぎます",
|
||||
"URL slug": "URL スラッグ",
|
||||
"Use a custom link instead of a random one": "ランダムなリンクではなく、独自のリンクを使う",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "表示されるのはこの一度きりです。ハッシュしか保存していないため、後から復元することはできません。",
|
||||
"Copy to clipboard": "クリップボードにコピー",
|
||||
"Copied": "コピーしました",
|
||||
"Your tokens": "あなたのトークン",
|
||||
"Revoking a token takes effect immediately.": "トークンの無効化はただちに反映されます。",
|
||||
"You have not created any tokens yet.": "まだトークンを作成していません。",
|
||||
"Expired": "期限切れ",
|
||||
"Never used": "未使用",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "クライアントのカスタムフィールドの管理",
|
||||
"Moderate comments": "コメントの管理",
|
||||
"Only me": "自分だけ",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "このファイルを開ける人全員に表示されます。ログインしていない訪問者も含みます。",
|
||||
"Anyone, including visitors who are not logged in": "誰でも (ログインしていない訪問者を含む)",
|
||||
"Staff and clients": "スタッフとクライアント",
|
||||
"Staff only": "スタッフのみ",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "新しいコメントを受け付けなくなっても、すでに書かれたコメントは読めるままです。",
|
||||
"Comments awaiting approval": "承認待ちのコメント",
|
||||
"Comment settings": "コメント設定",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "ログインしていない訪問者が残したコメントです。承認するまで誰にも表示されません。",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "新しいアップロードの保存先として、外部の S3 互換バケット (AWS S3、MinIO、その他の S3 互換サービス) を接続します。",
|
||||
"Custom endpoint": "カスタムエンドポイント",
|
||||
"Custom field created.": "カスタムフィールドを作成しました。",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "失敗したジョブはありません。",
|
||||
"No files match these filters.": "この条件に一致するファイルはありません。",
|
||||
"None of the selected files could be edited.": "選択したファイルはいずれも編集できませんでした。",
|
||||
"Nothing is waiting for approval.": "承認待ちのものはありません。",
|
||||
"of :count": "/ :count",
|
||||
"of :quota": "/ :quota",
|
||||
"on": "—",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "ページ",
|
||||
"Path prefix": "パスの接頭辞",
|
||||
"Pending": "保留中",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "projectsend.org の公開リポジトリを定期的に確認し、新しいバージョンがあればダッシュボードに通知します。自動でダウンロードや適用が行われることはありません。",
|
||||
"Permanently failed": "完全に失敗",
|
||||
"Post comment": "コメントを投稿",
|
||||
"Preview of :name": ":name のプレビュー",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "二要素認証を削除",
|
||||
"Remove two-factor authentication?": "二要素認証を削除しますか?",
|
||||
"Require a verified email address": "確認済みのメールアドレスを必須にする",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "これらは Web サーバーが動作しているユーザーとして実行してください。詳しい手順は INSTALL.md にあります。",
|
||||
"Score threshold": "スコアのしきい値",
|
||||
"Search timezones…": "タイムゾーンを検索…",
|
||||
"Security check failed. Please try again.": "セキュリティチェックに失敗しました。もう一度お試しください。",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "このサイトは reCAPTCHA によって保護されており、Google のプライバシーポリシーと利用規約が適用されます。",
|
||||
"Those files have reached their download limit.": "それらのファイルはダウンロード上限に達しました。",
|
||||
"Timezone": "タイムゾーン",
|
||||
"To update, back up your database and files, then:": "更新するには、データベースとファイルをバックアップしてから、次を実行します:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "更新するには INSTALL.md の手順に従ってください。まずバックアップし、次にリリースを展開してマイグレーションを実行します。",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "このサーバーが外部へのリクエストを一切行うべきでない場合にのみオフにしてください。サービスに接続できない場合でもパスワードは受け入れられるため、オフラインのインストールがこれによってブロックされることはありません。",
|
||||
"Two-factor authentication removed.": "二要素認証を削除しました。",
|
||||
"Two-factor authentication removed by an admin": "管理者によって二要素認証が削除されました",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "あなた自身のアカウントは :timezone に設定されているため、これによってあなたに表示される日付が変わることはありません。",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "「common」ではなく、あなた自身のテナントです。Microsoft ではユーザーがアカウントのメールアドレスを変更できるため、サインインが信頼できるのは、トークンがここで指定したテナントから来た場合だけです。",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "プロバイダーがこのインストールのシークレットキーを拒否したため、保護されたフォームはチェックなしで通過させています。下のシークレットキーを確認してください。",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "あなたの公開フォームは保護されています。いつでも自分のキーに切り替えられます。"
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "あなたの公開フォームは保護されています。いつでも自分のキーに切り替えられます。",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "意図的に以前のバージョンへ戻した場合は、php artisan projectsend:update を実行して、データベースを復元したコードに合わせてください。",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "リリースの確認前、ダウンロード前、そしてインストール環境に手を加える前に、そのつど確認を求めます。ダウンロードしたファイルのチェックサムも検証します。詳しい手順は UPDATE.md にあります。",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "新しいファイルは配置されていますが、データベースは :applied までしか更新されていません。更新を実行するまで、このインストール環境はデータベースと一致しないコードで動作します。",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "projectsend.org の公開リポジトリを定期的に確認し、新しいバージョンがあればダッシュボードに通知します。自動でダウンロードや適用が行われることはありません。更新は必ずあなたが実行するもので、更新スクリプトは各ステップの前に確認を求めます。",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "データベースは :applied に更新されましたが、PHP はそれ以前にコンパイルしたコードを配信し続けています。再読み込みすればこの通知は消えます。",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "データベースは :applied に更新されましたが、このコンテナは起動時のコードのまま動作しています。",
|
||||
"This server is running version :running, but version :applied is installed": "このサーバーはバージョン :running で動作していますが、インストールされているのはバージョン :applied です",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "更新するには、インストールディレクトリで sudo ./update.sh を実行してください。ダウンロードや変更を行う前に確認を求めます。",
|
||||
"To update, run this in the install directory:": "更新するには、インストールディレクトリで次を実行してください:",
|
||||
"Version :running is installed, but the update has not been run": "バージョン :running がインストールされていますが、更新は実行されていません",
|
||||
"Updated ProjectSend to :to, from :from": "ProjectSend を :from から :to に更新しました",
|
||||
"ProjectSend was updated to a new version": "ProjectSend が新しいバージョンに更新されました",
|
||||
"Come and say hello": "気軽に声をかけてください",
|
||||
"Continue to the dashboard": "ダッシュボードへ進む",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Discord に参加する",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend には Discord があります。リリースの最新情報、うまく動かないときのサポート、そして同じソフトウェアを使っている人たち。私たちもそこにいます。",
|
||||
"What's new": "新着情報",
|
||||
"What each release brought to this installation.": "各リリースがこのインストールにもたらしたもの。",
|
||||
"Add the rest of your team": "チームの残りのメンバーを追加",
|
||||
"Add your first client": "最初のクライアントを追加",
|
||||
"Check the scheduler is running": "スケジューラーが動いているか確認",
|
||||
"Choose how your email looks": "メールの見た目を選ぶ",
|
||||
"Choose how your file lists look": "ファイル一覧の見た目を選ぶ",
|
||||
"Drop one in and choose who gets it.": "ドロップして、届ける相手を選びます。",
|
||||
"Essential": "必須",
|
||||
"Expiring files and queued email need it.": "期限切れファイルや送信待ちのメールに必要です。",
|
||||
"Four layouts for the client-facing pages.": "クライアントに見えるページ用の 4 つのレイアウト。",
|
||||
"Getting started": "はじめに",
|
||||
"Group your clients": "クライアントをグループにまとめる",
|
||||
"Made with care in Argentina. Shared with the world.": "アルゼンチンで心を込めて。世界へ届けます。",
|
||||
"Previewed on a real message.": "実際のメッセージでプレビューできます。",
|
||||
"Resets and share links go out through it.": "パスワード再設定と共有リンクはここを通ります。",
|
||||
"Set up your mail server": "メールサーバーを設定する",
|
||||
"Share once instead of six times.": "6 回ではなく 1 回で共有できます。",
|
||||
"Staff accounts, each scoped by its role.": "それぞれの役割の範囲で使えるスタッフアカウント。",
|
||||
"Thank you for installing ProjectSend": "ProjectSend をインストールしていただきありがとうございます",
|
||||
"Thank you for updating ProjectSend": "ProjectSend を更新していただきありがとうございます",
|
||||
"The people you send files to.": "ファイルを送る相手です。",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "新しいインストールでやることの要点です。期限はありません。",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "現在 :version です。すべて元どおり動作しています。これからも ProjectSend にファイル共有をお任せいただきありがとうございます。",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": ":previous から :version になりました。すべて元どおり動作しています。これからも ProjectSend にファイル共有をお任せいただきありがとうございます。",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "バージョン :version を使っています。まず何をするとよいかをまとめました。",
|
||||
"Thank you for choosing ProjectSend": "ProjectSend をお選びいただきありがとうございます",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "サイトの準備が整いました。運用は私たちにお任せください。まず何をするとよいかをまとめました。",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 はすべてを保持します。未読の通知は、どれだけ古くても削除されません。",
|
||||
":version is available": ":version が利用可能です",
|
||||
":version on :date": ":version(:date)",
|
||||
"Check now": "今すぐ確認",
|
||||
"Checked a moment ago, so this is the answer from then.": "確認したのは少し前なので、これはその時点での結果です。",
|
||||
"Checking…": "確認中…",
|
||||
"Could not reach the update feed.": "更新フィードに接続できませんでした。",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "何かに手を付ける前にデータベースをダンプし、ダンプに失敗したら中止します。",
|
||||
"Housekeeping": "メンテナンス",
|
||||
"Keep failed jobs for": "失敗したジョブの保持期間",
|
||||
"Keep read notifications for": "既読の通知の保持期間",
|
||||
"Last checked :date": "最終確認: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "PHP とワーカーには手を触れません。自分で再起動する場合に。",
|
||||
"Other options": "その他のオプション",
|
||||
"Report which version is installed and which is available, and change nothing.": "インストール済みのバージョンと利用可能なバージョンを報告し、何も変更しません。",
|
||||
"Retention updated.": "保持期間を更新しました。",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "オプションなしで実行すると、スクリプトが進行に応じてこれらを一つずつ尋ねます。これらは、答えがすでに分かっている実行のためのものです。",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "バックアップの確認を省略します。付きっきりでない実行のために。",
|
||||
"The latest release (:tag) is not a version this can compare.": "最新リリース(:tag)は、ここで比較できるバージョンではありません。",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "自然に溜まっていくものが二つあります。上の日次の削除処理はこの期間に従います。",
|
||||
"Up to date": "最新です",
|
||||
"Update checks are not available on this edition.": "このエディションでは更新の確認を利用できません。",
|
||||
"Update options": "更新オプション",
|
||||
"Updated": "更新日",
|
||||
"Version :version is available. You are running :current.": "バージョン :version が利用可能です。現在は :current を使用しています。",
|
||||
"You are up to date, running version :current.": "最新の状態です。バージョン :current を使用しています。",
|
||||
"days": "日",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help ですべてのオプションを確認でき、UPDATE.md では手作業を含む手順全体を説明しています。",
|
||||
"Fetch dashboard news": "ダッシュボードのお知らせを取得",
|
||||
"Purge API request logs": "API リクエストログを削除",
|
||||
"Purge erased accounts": "消去済みアカウントを削除",
|
||||
"Purge expired files": "期限切れファイルを削除",
|
||||
"Purge failed jobs": "失敗したジョブを削除",
|
||||
"Purge orphan files": "孤立ファイルを削除",
|
||||
"Purge read notifications": "既読の通知を削除",
|
||||
"Purge stale chunked uploads": "中断された分割アップロードを削除",
|
||||
"Purge zip downloads": "ZIP ダウンロードを削除",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "このフィルターに一致: :count 件 (新しい順)",
|
||||
"A file was previewed via the public group listing": "公開グループ一覧からファイルがプレビューされました",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "リンクを知っている人は、画像・動画・音声ファイル・PDF をダウンロードせずにブラウザーで開けます。オフにすると、公開ファイルを見る方法はダウンロードだけになります。",
|
||||
"Clients can preview files": "クライアントはファイルをプレビューできます",
|
||||
"Downloads & previews": "ダウンロードとプレビュー",
|
||||
"Downloads: :count": "ダウンロード: :count",
|
||||
"Let visitors preview public files": "訪問者が公開ファイルをプレビューできるようにする",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "クライアントが画像・動画・音声ファイル・PDF をダウンロードするだけでなく、ポータル内で開けるようになります。オフにすると、ファイルを見る方法はダウンロードだけになります。スタッフは常にプレビューできます。",
|
||||
"No downloads match these filters.": "この条件に一致するダウンロードはありません。",
|
||||
"Nobody has downloaded or previewed this file yet.": "このファイルはまだ誰もダウンロードもプレビューもしていません。",
|
||||
"Preview :name": ":name をプレビュー",
|
||||
"Previewed the file \":subject\" via the public group listing": "公開グループ一覧からファイル「:subject」をプレビューしました",
|
||||
"Previews: :count": "プレビュー: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "このインストールはイメージを自分でビルドするため、イメージを取得しても何も変わりません。リリースで composer.lock やフロントエンドが変わっている場合は、composer install と npm run build も実行してください。詳しい手順は UPDATE.md にあります。",
|
||||
"To update, run this in the directory you cloned:": "更新するには、クローンしたディレクトリで次を実行してください:",
|
||||
"View all previews (:count)": "すべてのプレビューを表示 (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "お使いのブラウザーではこのファイルを再生できません。ダウンロードしてご覧ください。"
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count downloads",
|
||||
":count file deleted.|:count files deleted.": ":count bestand verwijderd.|:count bestanden verwijderd.",
|
||||
":count file imported.|:count files imported.": ":count bestand geïmporteerd.|:count bestanden geïmporteerd.",
|
||||
":count files": ":count bestanden",
|
||||
":count folders": ":count mappen",
|
||||
":count items have been shared with you": ":count items zijn met je gedeeld",
|
||||
":count members": ":count leden",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(en :count andere foutmelding)",
|
||||
"(and :count more errors)": "(en :count andere foutmelding)|(en :count andere foutmeldingen)|(en :count andere foutmeldingen)",
|
||||
"(deleted account)": "(verwijderd account)",
|
||||
"0 means unlimited.": "0 betekent onbeperkt.",
|
||||
"A client account request was approved": "Een aanvraag voor een klantaccount is goedgekeurd",
|
||||
"A client account request was denied": "Een aanvraag voor een klantaccount is geweigerd",
|
||||
"A client left a group": "Een klant heeft een groep verlaten",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Er is een nieuw klantaccount aangemaakt: :name (:email).",
|
||||
"A new client has registered": "Een nieuwe klant heeft zich geregistreerd",
|
||||
"A new ProjectSend version is available": "Er is een nieuwe versie van ProjectSend beschikbaar",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "De pagina van een openbare groep is altijd bereikbaar via de eigen link zodra je die als openbaar markeert — dit onderdeel bepaalt alleen de basis-URL en een optioneel overzicht dat alle openbare groepen en bestanden in één keer toont.",
|
||||
"A public link was created": "Er is een openbare link aangemaakt",
|
||||
"A public link was revoked": "Een openbare link is ingetrokken",
|
||||
"A role was created": "Een rol is aangemaakt",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Beheer: groepslidmaatschap aangevraagd",
|
||||
"Admin: new client registered": "Beheer: nieuwe klant geregistreerd",
|
||||
"Administration": "Beheer",
|
||||
"After uploading you can assign it to clients and groups": "Na het uploaden kun je het aan klanten en groepen toewijzen",
|
||||
"Agree": "Akkoord",
|
||||
"All": "Alle",
|
||||
"All actions": "Alle acties",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Alle reacties",
|
||||
"All failed jobs deleted.": "Alle mislukte taken zijn verwijderd.",
|
||||
"All files": "Alle bestanden",
|
||||
"All groups": "Alle groepen",
|
||||
"All permissions": "Alle rechten",
|
||||
"All rights reserved.": "Alle rechten voorbehouden.",
|
||||
"All roles": "Alle rollen",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Wie hem gebruikt, kan het bestand niet meer downloaden.",
|
||||
"Appearance": "Weergave",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Geldt voor de openbare groeps- en bestandspagina's voor bezoekers en voor het klantportaal \"Mijn bestanden\".",
|
||||
"Apply filters": "Filters toepassen",
|
||||
"Approve": "Goedkeuren",
|
||||
"Approve account requests": "Accountaanvragen goedkeuren",
|
||||
"Approve group membership requests": "Aanvragen voor groepslidmaatschap goedkeuren",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Weet je zeker dat je je account wilt verwijderen?",
|
||||
"Assign": "Toewijzen",
|
||||
"Assign categories to files": "Categorieën aan bestanden toewijzen",
|
||||
"Assigned": "Toegewezen",
|
||||
"Assigned clients": "Toegewezen klanten",
|
||||
"Assigned the file \":subject\" to :target": "Heeft het bestand \":subject\" toegewezen aan :target",
|
||||
"Associate": "Associëren",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Auteur",
|
||||
"Available groups": "Beschikbare groepen",
|
||||
"Available placeholders": "Beschikbare tijdelijke aanduidingen",
|
||||
"Back to My files": "Terug naar Mijn bestanden",
|
||||
"Bad Gateway": "Slechte poort",
|
||||
"Bad Request": "Foute aanvraag",
|
||||
"Bandwidth Limit Exceeded": "Bandbreedte overschreden",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Berichttekst",
|
||||
"Browse": "Bladeren",
|
||||
"Browse publicly shared groups and files below.": "Blader hieronder door openbaar gedeelde groepen en bestanden.",
|
||||
"Built-in": "Ingebouwd",
|
||||
"Built-in role names cannot be changed.": "De naam van ingebouwde rollen kan niet worden gewijzigd.",
|
||||
"Built-in roles cannot be deleted.": "Ingebouwde rollen kunnen niet worden verwijderd.",
|
||||
"Call to action": "Call-to-action",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Wachtwoord bevestigen",
|
||||
"Conflict": "Conflict",
|
||||
"Connect": "Verbinden",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Koppel een externe bucket — S3-compatibel (AWS S3, MinIO, Backblaze) of Google Cloud Storage — als opslag voor nieuwe uploads.",
|
||||
"Connection Closed Without Response": "Verbinding gesloten zonder reactie",
|
||||
"Connection Timed Out": "Connectie duurt te lang",
|
||||
"Continue": "Doorgaan",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Deactiveer je account nu; na :days dagen wordt het definitief gewist",
|
||||
"Deactivated the account \":subject\"": "Heeft het account \":subject\" gedeactiveerd",
|
||||
"Default": "Standaard",
|
||||
"Defaults to the file name.": "Standaard de bestandsnaam.",
|
||||
"Delete": "Verwijderen",
|
||||
"Delete :name": ":Name verwijderen",
|
||||
"Delete account": "Account verwijderen",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Weigeren",
|
||||
"Deny this request?": "Deze aanvraag weigeren?",
|
||||
"Description": "Omschrijving",
|
||||
"Description (optional)": "Omschrijving (optioneel)",
|
||||
"Detach": "Loskoppelen",
|
||||
"Details": "Informatie",
|
||||
"Disable": "Uitschakelen",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Ga naar pagina :page",
|
||||
"Go to the login form": "Naar het inlogformulier",
|
||||
"Gone": "Verdwenen",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Grijs",
|
||||
"Green": "Groen",
|
||||
"Group": "Groep",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Inloggen",
|
||||
"Logout": "Uitloggen",
|
||||
"Loop Detected": "Loop gedetecteerd",
|
||||
"Mail provider settings saved.": "Instellingen van de e-mailprovider opgeslagen.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Dienst niet beschikbaar",
|
||||
"Manage custom fields": "Aangepaste velden beheren",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Er zijn nog geen bestanden met je gedeeld.",
|
||||
"No files here yet.": "Hier staan nog geen bestanden.",
|
||||
"No files or folders match your search.": "Geen bestand of map komt overeen met je zoekopdracht.",
|
||||
"No files yet. Upload the first one.": "Nog geen bestanden. Upload het eerste.",
|
||||
"No folder": "Geen map",
|
||||
"No groups match these filters.": "Geen groep voldoet aan deze filters.",
|
||||
"No groups yet.": "Nog geen groepen.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Nog geen ontvangers toegevoegd.",
|
||||
"No requests match your search.": "Geen aanvraag komt overeen met je zoekopdracht.",
|
||||
"No roles match this filter.": "Geen rol voldoet aan dit filter.",
|
||||
"No service account key has been saved yet.": "Er is nog geen serviceaccountsleutel opgeslagen.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Geen groottelimiet. Uploads kunnen worden gepauzeerd en hervatten automatisch na een onderbreking.",
|
||||
"No users match these filters.": "Geen gebruiker voldoet aan deze filters.",
|
||||
"No users yet.": "Nog geen gebruikers.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Oranje",
|
||||
"Origin Is Unreachable": "Herkomst is onbereikbaar",
|
||||
"Outgoing email": "Uitgaande e-mail",
|
||||
"Outgoing email theme": "Thema voor uitgaande e-mail",
|
||||
"Page :current of :last": "Pagina :current van :last",
|
||||
"Page Expired": "Pagina niet meer geldig",
|
||||
"Pagination Navigation": "Paginanavigatie",
|
||||
"Partial Content": "Gedeeltelijke inhoud",
|
||||
"Password": "Wachtwoord",
|
||||
"Password reset": "Wachtwoordherstel",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Plak het JSON-sleutelbestand van een serviceaccount met lees- en schrijftoegang tot de objecten in de bucket. Het wordt versleuteld opgeslagen en nooit meer getoond.",
|
||||
"Path": "Pad",
|
||||
"Payload Too Large": "Aanvraag te groot",
|
||||
"Payment Required": "Betaling vereist",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Instellingen voor het openbare overzicht",
|
||||
"Public listing URL": "URL van het openbare overzicht",
|
||||
"Public pages & portal": "Openbare pagina's en portaal",
|
||||
"Public pages and client portal theme": "Thema voor openbare pagina's en klantportaal",
|
||||
"Published": "Gepubliceerd",
|
||||
"Purple": "Paars",
|
||||
"Quota": "Quotum",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Herstellen",
|
||||
"Restore :name": ":Name herstellen",
|
||||
"Restricted extension": "Beperkte extensie",
|
||||
"Result": "Resultaat",
|
||||
"results": "resultaten",
|
||||
"Review membership requests": "Lidmaatschapsaanvragen beoordelen",
|
||||
"Review pending requests": "Openstaande aanvragen beoordelen",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Rolnaam",
|
||||
"Role updated.": "Rol bijgewerkt.",
|
||||
"Roles": "Rollen",
|
||||
"S3-compatible": "S3-compatibel",
|
||||
"Sample email": "Voorbeeld-e-mail",
|
||||
"Save": "Opslaan",
|
||||
"Save :name": ":Name opslaan",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Verstuurt nu meteen een echte e-mail, buiten de wachtrij om, zodat je kunt controleren of de instellingen hierboven echt werken.",
|
||||
"Separate paragraphs with a blank line.": "Scheid alinea's met een lege regel.",
|
||||
"Server Error": "Serverfout",
|
||||
"Service account key": "Serviceaccountsleutel",
|
||||
"Service Unavailable": "Website onbeschikbaar",
|
||||
"Session Has Expired": "Pagina verlopen",
|
||||
"Set file expiration dates": "Vervaldatums voor bestanden instellen",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Instellingen",
|
||||
"Settings that apply to the whole installation": "Instellingen die voor de hele installatie gelden",
|
||||
"Setup complete": "Installatie voltooid",
|
||||
"Share a file with the people who manage your account.": "Deel een bestand met de mensen die je account beheren.",
|
||||
"Share files with several clients at once": "Deel bestanden in één keer met meerdere klanten",
|
||||
"Shared the folder \":subject\" with :target": "Heeft de map \":subject\" gedeeld met :target",
|
||||
"Shared with": "Gedeeld met",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Protocolwissel",
|
||||
"System": "Systeem",
|
||||
"System Administrator": "Systeembeheerder",
|
||||
"System settings": "Systeeminstellingen",
|
||||
"System settings were updated": "De systeeminstellingen zijn bijgewerkt",
|
||||
"System users": "Systeemgebruikers",
|
||||
"Tag": "Label",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Test",
|
||||
"Text": "Tekst",
|
||||
"Text or author": "Tekst of auteur",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Dit lijkt geen serviceaccountsleutelbestand: het is geen geldige JSON.",
|
||||
"That service account key file is missing its :field.": "In dit serviceaccountsleutelbestand ontbreekt :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Het account van :name wordt definitief verwijderd. Dit kan niet ongedaan worden gemaakt.",
|
||||
"The account password was changed": "Het wachtwoord van het account is gewijzigd",
|
||||
"The account request of :name will be denied and the account deleted.": "De accountaanvraag van :name wordt geweigerd en het account verwijderd.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "De groepen waar je bij hoort en de groepen waarvoor je je kunt aanmelden",
|
||||
"The header/footer style used for every outgoing notification email.": "De stijl van koptekst en voettekst voor elke uitgaande meldings-e-mail.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Het uiterlijk van de openbare bezoekerspagina's, het klantportaal en de uitgaande meldings-e-mails.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "De mailserver zelf stel je in het .env-bestand van deze installatie in (MAIL_*-variabelen), niet hier.",
|
||||
"The people who administer this installation and upload files": "De mensen die deze installatie beheren en bestanden uploaden",
|
||||
"The people you share files with": "De mensen met wie je bestanden deelt",
|
||||
"The provided two-factor authentication code was invalid.": "De ingevoerde code voor tweefactorauthenticatie is ongeldig.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Er valt nog niets in te stellen.",
|
||||
"Test email from :site": "Test-e-mail van :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Deze bestanden worden definitief van schijf verwijderd. Dit kan niet ongedaan worden gemaakt.",
|
||||
"Theme": "Thema",
|
||||
"Theme updated.": "Thema bijgewerkt.",
|
||||
"Theming": "Thema's",
|
||||
"Theming settings": "Thema-instellingen",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Deze upload zou je opslagquotum van :quota MB overschrijden.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Tip: sleep bestanden en mappen op een map of op het kruimelpad om ze te verplaatsen.",
|
||||
"Title": "Titel",
|
||||
"Title (optional)": "Titel (optioneel)",
|
||||
"TLS": "TLS",
|
||||
"to": "tot",
|
||||
"To": "Aan",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Te veel bestanden geselecteerd. Kies een kleinere selectie en probeer het opnieuw.",
|
||||
"Too Many Requests": "Te veel serververzoeken",
|
||||
"Transactional email settings for this installation": "Instellingen voor transactionele e-mail van deze installatie",
|
||||
"Transfers — last 30 days": "Overdrachten — afgelopen 30 dagen",
|
||||
"Translate": "Vertalen",
|
||||
"Translate It": "Vertalen",
|
||||
"Turns all transactional email for this installation on or off.": "Zet alle transactionele e-mail van deze installatie aan of uit.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Heeft het bestand \":subject\" geüpload",
|
||||
"Uploader": "Uploader",
|
||||
"Uploads": "Uploads",
|
||||
"Uploads (chart)": "Uploads (grafiek)",
|
||||
"URI Too Long": "Aanvraag-URL te lang",
|
||||
"URL slug": "URL-slug",
|
||||
"Use a custom link instead of a random one": "Een eigen link gebruiken in plaats van een willekeurige",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Dit is de enige keer dat het wordt getoond. We bewaren alleen een hash, dus het kan later niet worden teruggehaald.",
|
||||
"Copy to clipboard": "Naar klembord kopiëren",
|
||||
"Copied": "Gekopieerd",
|
||||
"Your tokens": "Jouw tokens",
|
||||
"Revoking a token takes effect immediately.": "Het intrekken van een token gaat meteen in.",
|
||||
"You have not created any tokens yet.": "Je hebt nog geen tokens aangemaakt.",
|
||||
"Expired": "Verlopen",
|
||||
"Never used": "Nooit gebruikt",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Aangepaste klantvelden beheren",
|
||||
"Moderate comments": "Reacties modereren",
|
||||
"Only me": "Alleen ik",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Zichtbaar voor iedereen die dit bestand kan openen, ook voor bezoekers die niet ingelogd zijn.",
|
||||
"Anyone, including visitors who are not logged in": "Iedereen, ook bezoekers die niet ingelogd zijn",
|
||||
"Staff and clients": "Team en klanten",
|
||||
"Staff only": "Alleen het team",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Al geschreven reacties blijven leesbaar, ook als een bestand geen nieuwe meer aanneemt.",
|
||||
"Comments awaiting approval": "Reacties die op goedkeuring wachten",
|
||||
"Comment settings": "Reactie-instellingen",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Reacties van bezoekers die niet ingelogd zijn. Niemand ziet ze totdat jij ze goedkeurt.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Koppel een externe S3-compatibele bucket (AWS S3, MinIO of een andere S3-compatibele dienst) als opslag voor nieuwe uploads.",
|
||||
"Custom endpoint": "Eigen endpoint",
|
||||
"Custom field created.": "Aangepast veld aangemaakt.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Geen mislukte taken.",
|
||||
"No files match these filters.": "Geen bestand voldoet aan deze filters.",
|
||||
"None of the selected files could be edited.": "Geen van de geselecteerde bestanden kon worden bewerkt.",
|
||||
"Nothing is waiting for approval.": "Er wacht niets op goedkeuring.",
|
||||
"of :count": "van :count",
|
||||
"of :quota": "van :quota",
|
||||
"on": "op",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Pagina",
|
||||
"Path prefix": "Padvoorvoegsel",
|
||||
"Pending": "In behandeling",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Controleert periodiek of de openbare repository van projectsend.org een nieuwere versie heeft en toont een melding in het overzicht. Er wordt nooit automatisch iets gedownload of doorgevoerd.",
|
||||
"Permanently failed": "Definitief mislukt",
|
||||
"Post comment": "Reactie plaatsen",
|
||||
"Preview of :name": "Voorbeeld van :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Tweefactorauthenticatie verwijderen",
|
||||
"Remove two-factor authentication?": "Tweefactorauthenticatie verwijderen?",
|
||||
"Require a verified email address": "Een geverifieerd e-mailadres vereisen",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Voer deze uit als de gebruiker waaronder je webserver draait. INSTALL.md bevat de volledige procedure.",
|
||||
"Score threshold": "Scoredrempel",
|
||||
"Search timezones…": "Tijdzones zoeken…",
|
||||
"Security check failed. Please try again.": "De beveiligingscontrole is mislukt. Probeer het opnieuw.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Deze site wordt beschermd door reCAPTCHA en het Privacybeleid en de Servicevoorwaarden van Google zijn van toepassing.",
|
||||
"Those files have reached their download limit.": "Die bestanden hebben hun downloadlimiet bereikt.",
|
||||
"Timezone": "Tijdzone",
|
||||
"To update, back up your database and files, then:": "Maak om bij te werken een back-up van je database en bestanden, en daarna:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Volg om bij te werken de stappen in INSTALL.md — maak eerst een back-up, pak daarna de release uit en voer de migraties uit.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Zet dit alleen uit als deze server geen uitgaande verzoeken mag doen. Als de dienst niet bereikbaar is, wordt het wachtwoord toch geaccepteerd, zodat een offline installatie er nooit door wordt geblokkeerd.",
|
||||
"Two-factor authentication removed.": "Tweefactorauthenticatie verwijderd.",
|
||||
"Two-factor authentication removed by an admin": "Tweefactorauthenticatie verwijderd door een beheerder",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Je eigen account staat op :timezone, dus dit verandert niet hoe datums er voor jou uitzien.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Je eigen tenant, niet \"common\". Microsoft laat een gebruiker het e-mailadres van het account wijzigen, dus een aanmelding is alleen betrouwbaar wanneer het token afkomstig is van de tenant die je hier hebt opgegeven.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Je provider heeft de geheime sleutel van deze installatie geweigerd, dus beschermde formulieren worden ongecontroleerd doorgelaten. Controleer de geheime sleutel hieronder.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Je openbare formulieren zijn beschermd. Je kunt op elk moment overschakelen naar je eigen sleutels."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Je openbare formulieren zijn beschermd. Je kunt op elk moment overschakelen naar je eigen sleutels.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Als je bewust bent teruggegaan naar een oudere versie, voer dan php artisan projectsend:update uit zodat de database past bij de code die je hebt teruggezet.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Het vraagt het je voordat het naar een versie zoekt, voordat het er een downloadt en voordat het je installatie aanraakt — en controleert de checksum van wat het downloadt. UPDATE.md bevat de volledige procedure.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "De nieuwe bestanden staan klaar, maar de database is alleen bijgewerkt tot :applied. Zolang de update niet is uitgevoerd, draait deze installatie op code die niet bij de database past.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Controleert periodiek of de openbare repository van projectsend.org een nieuwere versie heeft en toont een melding in het overzicht. Er wordt nooit automatisch iets gedownload of doorgevoerd: bijwerken doe je altijd zelf, en het updatescript vraagt het je voor elke stap.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "De database is bijgewerkt naar :applied, en PHP serveert nog steeds de code die het daarvoor had gecompileerd. Opnieuw laden laat deze melding verdwijnen.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "De database is bijgewerkt naar :applied, en deze container draait nog steeds de code waarmee hij is gestart.",
|
||||
"This server is running version :running, but version :applied is installed": "Deze server draait versie :running, maar versie :applied is geïnstalleerd",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Voer om bij te werken sudo ./update.sh uit in de installatiemap — het vraagt het je voordat het iets downloadt of wijzigt.",
|
||||
"To update, run this in the install directory:": "Voer dit uit in de installatiemap om bij te werken:",
|
||||
"Version :running is installed, but the update has not been run": "Versie :running is geïnstalleerd, maar de update is niet uitgevoerd",
|
||||
"Updated ProjectSend to :to, from :from": "Heeft ProjectSend bijgewerkt naar :to, vanaf :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend is bijgewerkt naar een nieuwe versie",
|
||||
"Come and say hello": "Kom even hallo zeggen",
|
||||
"Continue to the dashboard": "Verder naar het overzicht",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Word lid van de Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend heeft een Discord: nieuws over releases, hulp als iets zich vreemd gedraagt, en andere mensen die dezelfde software draaien. Wij zitten er ook.",
|
||||
"What's new": "Wat is er nieuw",
|
||||
"What each release brought to this installation.": "Wat elke release deze installatie heeft gebracht.",
|
||||
"Add the rest of your team": "Voeg de rest van je team toe",
|
||||
"Add your first client": "Voeg je eerste klant toe",
|
||||
"Check the scheduler is running": "Controleer of de planner draait",
|
||||
"Choose how your email looks": "Kies hoe je e-mail eruitziet",
|
||||
"Choose how your file lists look": "Kies hoe je bestandslijsten eruitzien",
|
||||
"Drop one in and choose who gets it.": "Zet er een neer en kies wie hem krijgt.",
|
||||
"Essential": "Onmisbaar",
|
||||
"Expiring files and queued email need it.": "Verlopende bestanden en e-mail in de wachtrij hebben het nodig.",
|
||||
"Four layouts for the client-facing pages.": "Vier indelingen voor de pagina's die klanten zien.",
|
||||
"Getting started": "Aan de slag",
|
||||
"Group your clients": "Groepeer je klanten",
|
||||
"Made with care in Argentina. Shared with the world.": "Met zorg gemaakt in Argentinië. Gedeeld met de wereld.",
|
||||
"Previewed on a real message.": "Bekeken op een echt bericht.",
|
||||
"Resets and share links go out through it.": "Wachtwoordherstel en deellinks gaan er doorheen.",
|
||||
"Set up your mail server": "Stel je mailserver in",
|
||||
"Share once instead of six times.": "Deel één keer in plaats van zes.",
|
||||
"Staff accounts, each scoped by its role.": "Teamaccounts, elk begrensd door hun rol.",
|
||||
"Thank you for installing ProjectSend": "Bedankt voor het installeren van ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Bedankt voor het bijwerken van ProjectSend",
|
||||
"The people you send files to.": "De mensen naar wie je bestanden stuurt.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "De korte versie van wat je op een nieuwe installatie doet. Niets hiervan verloopt.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Je draait nu op :version en alles draait weer. Bedankt dat je je bestandsdeling aan ProjectSend blijft toevertrouwen.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Je draait nu op :version, vanaf :previous, en alles draait weer. Bedankt dat je je bestandsdeling aan ProjectSend blijft toevertrouwen.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Je draait versie :version, en die is van jou. Dit is wat je het eerst wilt doen.",
|
||||
"Thank you for choosing ProjectSend": "Bedankt dat je voor ProjectSend hebt gekozen",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Je site is klaar, en hem draaiende houden is onze taak. Dit is wat je het eerst wilt doen.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 bewaart alles. Ongelezen meldingen worden nooit verwijderd, hoe oud ze ook zijn.",
|
||||
":version is available": ":version is beschikbaar",
|
||||
":version on :date": ":version op :date",
|
||||
"Check now": "Nu controleren",
|
||||
"Checked a moment ago, so this is the answer from then.": "De controle is van zojuist, dus dit is het antwoord van dat moment.",
|
||||
"Checking…": "Bezig met controleren…",
|
||||
"Could not reach the update feed.": "Kon de updatefeed niet bereiken.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Maak een dump van de database voordat je iets aanraakt, en stop als de dump mislukt.",
|
||||
"Housekeeping": "Onderhoud",
|
||||
"Keep failed jobs for": "Mislukte taken bewaren gedurende",
|
||||
"Keep read notifications for": "Gelezen meldingen bewaren gedurende",
|
||||
"Last checked :date": "Laatst gecontroleerd: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "PHP en de worker met rust laten, voor als je ze zelf herstart.",
|
||||
"Other options": "Andere opties",
|
||||
"Report which version is installed and which is available, and change nothing.": "Meldt welke versie geïnstalleerd is en welke beschikbaar is, en verandert niets.",
|
||||
"Retention updated.": "Bewaartermijn bijgewerkt.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Zonder opties uitgevoerd vraagt het script onderweg naar elk van deze dingen. Deze zijn voor uitvoeringen waarbij je het antwoord al weet.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Slaat de vraag over back-ups over, voor een uitvoering waar je niet bij zit.",
|
||||
"The latest release (:tag) is not a version this can compare.": "De nieuwste release (:tag) is geen versie die hier te vergelijken valt.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Twee dingen stapelen zich vanzelf op. Dit zijn de termijnen waar de dagelijkse opruimingen hierboven zich aan houden.",
|
||||
"Up to date": "Up-to-date",
|
||||
"Update checks are not available on this edition.": "Updatecontroles zijn niet beschikbaar in deze editie.",
|
||||
"Update options": "Update-opties",
|
||||
"Updated": "Bijgewerkt",
|
||||
"Version :version is available. You are running :current.": "Versie :version is beschikbaar. Je gebruikt :current.",
|
||||
"You are up to date, running version :current.": "Je bent up-to-date, met versie :current.",
|
||||
"days": "dagen",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help toont elke optie, en UPDATE.md loopt de hele procedure door, ook handmatig.",
|
||||
"Fetch dashboard news": "Nieuws voor het dashboard ophalen",
|
||||
"Purge API request logs": "API-aanvraaglogboek opschonen",
|
||||
"Purge erased accounts": "Gewiste accounts opschonen",
|
||||
"Purge expired files": "Verlopen bestanden opschonen",
|
||||
"Purge failed jobs": "Mislukte taken opschonen",
|
||||
"Purge orphan files": "Verweesde bestanden opschonen",
|
||||
"Purge read notifications": "Gelezen meldingen opschonen",
|
||||
"Purge stale chunked uploads": "Afgebroken deel-uploads opschonen",
|
||||
"Purge zip downloads": "ZIP-downloads opschonen",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Treffers voor dit filter: :count, nieuwste eerst",
|
||||
"A file was previewed via the public group listing": "Een bestand is via het openbare groepsoverzicht in het voorbeeld bekeken",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Iedereen met de link kan een afbeelding, video, audiobestand of PDF in de browser openen in plaats van het te downloaden. Zet dit uit als downloaden de enige manier moet zijn om een openbaar bestand te bekijken.",
|
||||
"Clients can preview files": "Klanten kunnen bestanden in voorvertoning bekijken",
|
||||
"Downloads & previews": "Downloads en voorvertoningen",
|
||||
"Downloads: :count": "Downloads: :count",
|
||||
"Let visitors preview public files": "Bezoekers openbare bestanden laten bekijken in voorvertoning",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Laat klanten een afbeelding, video, audiobestand of PDF in het portaal openen in plaats van het alleen te downloaden. Zet dit uit als downloaden de enige manier moet zijn om een bestand te bekijken. Het team kan altijd een voorvertoning bekijken.",
|
||||
"No downloads match these filters.": "Geen download voldoet aan deze filters.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Niemand heeft dit bestand nog gedownload of in het voorbeeld bekeken.",
|
||||
"Preview :name": ":name in voorvertoning bekijken",
|
||||
"Previewed the file \":subject\" via the public group listing": "Heeft het bestand \":subject\" via het openbare groepsoverzicht in het voorbeeld bekeken",
|
||||
"Previews: :count": "Voorvertoningen: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Deze installatie bouwt zijn eigen images, dus een image ophalen verandert niets. Heeft de release composer.lock of de frontend gewijzigd, voer dan ook composer install en npm run build uit — UPDATE.md beschrijft de volledige procedure.",
|
||||
"To update, run this in the directory you cloned:": "Voer dit uit in de map die je hebt gekloond om bij te werken:",
|
||||
"View all previews (:count)": "Alle voorvertoningen bekijken (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Je browser kan dit bestand niet afspelen. Download het om het te bekijken."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": "Pobrania: :count",
|
||||
":count file deleted.|:count files deleted.": "Usunięto :count plik.|Usunięto :count plików.",
|
||||
":count file imported.|:count files imported.": "Zaimportowano :count plik.|Zaimportowano :count plików.",
|
||||
":count files": "Pliki: :count",
|
||||
":count folders": "Foldery: :count",
|
||||
":count items have been shared with you": "Liczba udostępnionych Ci elementów: :count",
|
||||
":count members": "Członkowie: :count",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(i jeszcze :count błąd)",
|
||||
"(and :count more errors)": "(i jeszcze :count błąd)|(i jeszcze :count błędy)|(i jeszcze :count błędów)",
|
||||
"(deleted account)": "(usunięte konto)",
|
||||
"0 means unlimited.": "0 oznacza brak limitu.",
|
||||
"A client account request was approved": "Zatwierdzono wniosek o konto klienta",
|
||||
"A client account request was denied": "Odrzucono wniosek o konto klienta",
|
||||
"A client left a group": "Klient opuścił grupę",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Utworzono nowe konto klienta: :name (:email).",
|
||||
"A new client has registered": "Zarejestrował się nowy klient",
|
||||
"A new ProjectSend version is available": "Dostępna jest nowa wersja ProjectSend",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Strona grupy publicznej jest zawsze dostępna pod własnym linkiem, gdy oznaczysz ją jako publiczną — ta sekcja steruje wyłącznie adresem bazowym oraz opcjonalnym katalogiem, który pokazuje naraz wszystkie publiczne grupy i pliki.",
|
||||
"A public link was created": "Utworzono link publiczny",
|
||||
"A public link was revoked": "Unieważniono link publiczny",
|
||||
"A role was created": "Utworzono rolę",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Administracja: złożono wniosek o członkostwo w grupie",
|
||||
"Admin: new client registered": "Administracja: zarejestrował się nowy klient",
|
||||
"Administration": "Administracja",
|
||||
"After uploading you can assign it to clients and groups": "Po przesłaniu możesz przypisać go klientom i grupom",
|
||||
"Agree": "Zgadzam się",
|
||||
"All": "Wszystkie",
|
||||
"All actions": "Wszystkie działania",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Wszystkie komentarze",
|
||||
"All failed jobs deleted.": "Wszystkie nieudane zadania zostały usunięte.",
|
||||
"All files": "Wszystkie pliki",
|
||||
"All groups": "Wszystkie grupy",
|
||||
"All permissions": "Wszystkie uprawnienia",
|
||||
"All rights reserved.": "Wszelkie prawa zastrzeżone.",
|
||||
"All roles": "Wszystkie role",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Nikt, kto go używa, nie pobierze już tego pliku.",
|
||||
"Appearance": "Wygląd",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Dotyczy publicznych stron grup i plików dla odwiedzających oraz portalu klienta „Moje pliki”.",
|
||||
"Apply filters": "Zastosuj filtry",
|
||||
"Approve": "Zatwierdź",
|
||||
"Approve account requests": "Zatwierdzanie wniosków o konto",
|
||||
"Approve group membership requests": "Zatwierdzanie wniosków o członkostwo w grupach",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Czy na pewno chcesz usunąć swoje konto?",
|
||||
"Assign": "Przydziel",
|
||||
"Assign categories to files": "Przypisywanie kategorii do plików",
|
||||
"Assigned": "Przypisano",
|
||||
"Assigned clients": "Przypisani klienci",
|
||||
"Assigned the file \":subject\" to :target": "Przypisał(a) plik \":subject\" do :target",
|
||||
"Associate": "Współpracownik",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Autor",
|
||||
"Available groups": "Dostępne grupy",
|
||||
"Available placeholders": "Dostępne symbole zastępcze",
|
||||
"Back to My files": "Powrót do Moich plików",
|
||||
"Bad Gateway": "Zła brama",
|
||||
"Bad Request": "Nieprawidłowe żądanie",
|
||||
"Bandwidth Limit Exceeded": "Przekroczono limit transferu danych",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Treść wiadomości",
|
||||
"Browse": "Przeglądaj",
|
||||
"Browse publicly shared groups and files below.": "Przeglądaj poniżej publicznie udostępnione grupy i pliki.",
|
||||
"Built-in": "Wbudowana",
|
||||
"Built-in role names cannot be changed.": "Nazw wbudowanych ról nie można zmieniać.",
|
||||
"Built-in roles cannot be deleted.": "Wbudowanych ról nie można usuwać.",
|
||||
"Call to action": "Wezwanie do działania",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Potwierdź hasło",
|
||||
"Conflict": "Konflikt",
|
||||
"Connect": "Połącz",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Podłącz zewnętrzny bucket — zgodny z S3 (AWS S3, MinIO, Backblaze) lub Google Cloud Storage — jako magazyn dla nowych przesyłanych plików.",
|
||||
"Connection Closed Without Response": "Połączenie zamknięte bez odpowiedzi",
|
||||
"Connection Timed Out": "Przekroczono limit czasu połączenia",
|
||||
"Continue": "Kontynuuj",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Dezaktywuj konto teraz; zostanie trwale wymazane po :days dniach",
|
||||
"Deactivated the account \":subject\"": "Dezaktywował(a) konto \":subject\"",
|
||||
"Default": "Domyślne",
|
||||
"Defaults to the file name.": "Domyślnie nazwa pliku.",
|
||||
"Delete": "Usuń",
|
||||
"Delete :name": "Usuń :name",
|
||||
"Delete account": "Usuń konto",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Odrzuć",
|
||||
"Deny this request?": "Odrzucić ten wniosek?",
|
||||
"Description": "Opis",
|
||||
"Description (optional)": "Opis (opcjonalnie)",
|
||||
"Detach": "Odłącz",
|
||||
"Details": "Szczegóły",
|
||||
"Disable": "Wyłącz",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Przejdź do strony :page",
|
||||
"Go to the login form": "Przejdź do formularza logowania",
|
||||
"Gone": "Zasób został usunięty lub przeniesiony",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Szary",
|
||||
"Green": "Zielony",
|
||||
"Group": "Grupa",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Logowanie",
|
||||
"Logout": "Wyloguj",
|
||||
"Loop Detected": "Wykryto pętlę",
|
||||
"Mail provider settings saved.": "Zapisano ustawienia dostawcy poczty.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Tryb konserwacji",
|
||||
"Manage custom fields": "Zarządzanie polami niestandardowymi",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Nie udostępniono Ci jeszcze żadnych plików.",
|
||||
"No files here yet.": "Nie ma tu jeszcze plików.",
|
||||
"No files or folders match your search.": "Żaden plik ani folder nie pasuje do wyszukiwania.",
|
||||
"No files yet. Upload the first one.": "Nie ma jeszcze plików. Prześlij pierwszy.",
|
||||
"No folder": "Brak folderu",
|
||||
"No groups match these filters.": "Żadna grupa nie pasuje do tych filtrów.",
|
||||
"No groups yet.": "Nie ma jeszcze grup.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Nie dodano jeszcze żadnych odbiorców.",
|
||||
"No requests match your search.": "Żaden wniosek nie pasuje do wyszukiwania.",
|
||||
"No roles match this filter.": "Żadna rola nie pasuje do tego filtru.",
|
||||
"No service account key has been saved yet.": "Nie zapisano jeszcze żadnego klucza konta usługi.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Bez limitu rozmiaru. Przesyłanie można wstrzymać, a po przerwie wznawia się automatycznie.",
|
||||
"No users match these filters.": "Żaden użytkownik nie pasuje do tych filtrów.",
|
||||
"No users yet.": "Nie ma jeszcze użytkowników.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Pomarańczowy",
|
||||
"Origin Is Unreachable": "Serwer źródłowy jest nieosiągalny",
|
||||
"Outgoing email": "Poczta wychodząca",
|
||||
"Outgoing email theme": "Motyw poczty wychodzącej",
|
||||
"Page :current of :last": "Strona :current z :last",
|
||||
"Page Expired": "Strona Wygasła",
|
||||
"Pagination Navigation": "Nawigacja Stron",
|
||||
"Partial Content": "Częściowa zawartość",
|
||||
"Password": "Hasło",
|
||||
"Password reset": "Resetowanie hasła",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Wklej plik klucza JSON konta usługi z prawem odczytu i zapisu obiektów w buckecie. Jest przechowywany w postaci zaszyfrowanej i nigdy więcej nie zostanie pokazany.",
|
||||
"Path": "Ścieżka",
|
||||
"Payload Too Large": "Ładunek zbyt duży",
|
||||
"Payment Required": "Płatność Wymagana",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Ustawienia listy publicznej",
|
||||
"Public listing URL": "Adres URL listy publicznej",
|
||||
"Public pages & portal": "Strony publiczne i portal",
|
||||
"Public pages and client portal theme": "Motyw stron publicznych i portalu klienta",
|
||||
"Published": "Opublikowano",
|
||||
"Purple": "Fioletowy",
|
||||
"Quota": "Limit",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Przywróć",
|
||||
"Restore :name": "Przywróć :name",
|
||||
"Restricted extension": "Ograniczone rozszerzenie",
|
||||
"Result": "Wynik",
|
||||
"results": "wyników",
|
||||
"Review membership requests": "Rozpatrywanie wniosków o członkostwo",
|
||||
"Review pending requests": "Rozpatrz oczekujące wnioski",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Nazwa roli",
|
||||
"Role updated.": "Zaktualizowano rolę.",
|
||||
"Roles": "Role",
|
||||
"S3-compatible": "Zgodny z S3",
|
||||
"Sample email": "Przykładowa wiadomość",
|
||||
"Save": "Zapisz",
|
||||
"Save :name": "Zapisz :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Wysyła od razu prawdziwą wiadomość, z pominięciem kolejki, żebyś mógł sprawdzić, czy powyższe ustawienia naprawdę działają.",
|
||||
"Separate paragraphs with a blank line.": "Oddzielaj akapity pustym wierszem.",
|
||||
"Server Error": "Błąd Serwera",
|
||||
"Service account key": "Klucz konta usługi",
|
||||
"Service Unavailable": "Serwis Niedostępny",
|
||||
"Session Has Expired": "Sesja wygasła – wymagane ponowne logowanie",
|
||||
"Set file expiration dates": "Ustawianie dat wygaśnięcia plików",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Ustawienia",
|
||||
"Settings that apply to the whole installation": "Ustawienia obowiązujące w całej instalacji",
|
||||
"Setup complete": "Konfiguracja zakończona",
|
||||
"Share a file with the people who manage your account.": "Udostępnij plik osobom, które zarządzają Twoim kontem.",
|
||||
"Share files with several clients at once": "Udostępniaj pliki kilku klientom naraz",
|
||||
"Shared the folder \":subject\" with :target": "Udostępnił(a) folder \":subject\" odbiorcy :target",
|
||||
"Shared with": "Udostępniono",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Przełączanie protokołów",
|
||||
"System": "System",
|
||||
"System Administrator": "Administrator systemu",
|
||||
"System settings": "Ustawienia systemu",
|
||||
"System settings were updated": "Zaktualizowano ustawienia systemu",
|
||||
"System users": "Użytkownicy systemu",
|
||||
"Tag": "Tag",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Test",
|
||||
"Text": "Tekst",
|
||||
"Text or author": "Treść lub autor",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "To nie wygląda na plik klucza konta usługi: to nie jest prawidłowy JSON.",
|
||||
"That service account key file is missing its :field.": "W tym pliku klucza konta usługi brakuje :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Konto :name zostanie trwale usunięte. Tej operacji nie można cofnąć.",
|
||||
"The account password was changed": "Zmieniono hasło do konta",
|
||||
"The account request of :name will be denied and the account deleted.": "Wniosek o konto od :name zostanie odrzucony, a konto usunięte.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Grupy, do których należysz, oraz te, o dołączenie do których możesz poprosić",
|
||||
"The header/footer style used for every outgoing notification email.": "Styl nagłówka i stopki używany w każdej wychodzącej wiadomości z powiadomieniem.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Wygląd publicznych stron dla odwiedzających, portalu klienta oraz wychodzących wiadomości z powiadomieniami.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Sam serwer pocztowy konfiguruje się w pliku .env tej instalacji (zmienne MAIL_*), a nie tutaj.",
|
||||
"The people who administer this installation and upload files": "Osoby, które administrują tą instalacją i przesyłają pliki",
|
||||
"The people you share files with": "Osoby, którym udostępniasz pliki",
|
||||
"The provided two-factor authentication code was invalid.": "Podany kod uwierzytelniania dwuskładnikowego był nieprawidłowy.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Nie ma jeszcze czego konfigurować.",
|
||||
"Test email from :site": "Wiadomość testowa z :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Te pliki zostaną trwale usunięte z dysku. Tej operacji nie można cofnąć.",
|
||||
"Theme": "Motyw",
|
||||
"Theme updated.": "Zaktualizowano motyw.",
|
||||
"Theming": "Motywy",
|
||||
"Theming settings": "Ustawienia motywów",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "To przesłanie przekroczyłoby Twój limit miejsca wynoszący :quota MB.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Wskazówka: przeciągnij pliki i foldery na folder lub na ścieżkę nawigacji, aby je przenieść.",
|
||||
"Title": "Tytuł",
|
||||
"Title (optional)": "Tytuł (opcjonalnie)",
|
||||
"TLS": "TLS",
|
||||
"to": "do",
|
||||
"To": "Do",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Zaznaczono zbyt wiele plików. Wybierz mniej i spróbuj ponownie.",
|
||||
"Too Many Requests": "Zbyt Dużo Zapytań",
|
||||
"Transactional email settings for this installation": "Ustawienia wiadomości transakcyjnych tej instalacji",
|
||||
"Transfers — last 30 days": "Transfery — ostatnie 30 dni",
|
||||
"Translate": "Przetłumacz",
|
||||
"Translate It": "Przetłumacz to",
|
||||
"Turns all transactional email for this installation on or off.": "Włącza lub wyłącza wszystkie wiadomości transakcyjne tej instalacji.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Przesłał(a) plik \":subject\"",
|
||||
"Uploader": "Osoba przesyłająca",
|
||||
"Uploads": "Przesłane pliki",
|
||||
"Uploads (chart)": "Przesłane pliki (wykres)",
|
||||
"URI Too Long": "Zbyt długi identyfikator URI",
|
||||
"URL slug": "Identyfikator URL",
|
||||
"Use a custom link instead of a random one": "Użyj własnego linku zamiast losowego",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "To jedyny raz, kiedy go pokazujemy. Przechowujemy tylko skrót, więc później nie da się go odzyskać.",
|
||||
"Copy to clipboard": "Kopiuj do schowka",
|
||||
"Copied": "Skopiowano",
|
||||
"Your tokens": "Twoje tokeny",
|
||||
"Revoking a token takes effect immediately.": "Unieważnienie tokena działa natychmiast.",
|
||||
"You have not created any tokens yet.": "Nie utworzyłeś jeszcze żadnych tokenów.",
|
||||
"Expired": "Wygasł",
|
||||
"Never used": "Nigdy nieużywany",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Zarządzanie polami niestandardowymi klientów",
|
||||
"Moderate comments": "Moderowanie komentarzy",
|
||||
"Only me": "Tylko ja",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Widoczne dla każdego, kto może otworzyć ten plik, także dla niezalogowanych odwiedzających.",
|
||||
"Anyone, including visitors who are not logged in": "Każdy, także niezalogowani odwiedzający",
|
||||
"Staff and clients": "Zespół i klienci",
|
||||
"Staff only": "Tylko zespół",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Napisane już komentarze pozostają czytelne, nawet gdy plik przestaje przyjmować nowe.",
|
||||
"Comments awaiting approval": "Komentarze oczekujące na zatwierdzenie",
|
||||
"Comment settings": "Ustawienia komentarzy",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Komentarze zostawione przez niezalogowanych odwiedzających. Nikt ich nie zobaczy, dopóki ich nie zatwierdzisz.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Podłącz zewnętrzny bucket zgodny z S3 (AWS S3, MinIO lub inną usługę zgodną z S3) jako magazyn dla nowych przesyłanych plików.",
|
||||
"Custom endpoint": "Własny punkt końcowy",
|
||||
"Custom field created.": "Utworzono pole niestandardowe.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Brak nieudanych zadań.",
|
||||
"No files match these filters.": "Żaden plik nie pasuje do tych filtrów.",
|
||||
"None of the selected files could be edited.": "Nie udało się edytować żadnego z zaznaczonych plików.",
|
||||
"Nothing is waiting for approval.": "Nic nie czeka na zatwierdzenie.",
|
||||
"of :count": "z :count",
|
||||
"of :quota": "z :quota",
|
||||
"on": "dnia",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Strona",
|
||||
"Path prefix": "Prefiks ścieżki",
|
||||
"Pending": "Oczekuje",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Okresowo sprawdza publiczne repozytorium projectsend.org pod kątem nowszego wydania i pokazuje informację na pulpicie. Nic nie jest nigdy pobierane ani instalowane automatycznie.",
|
||||
"Permanently failed": "Ostatecznie nieudane",
|
||||
"Post comment": "Opublikuj komentarz",
|
||||
"Preview of :name": "Podgląd :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Usuń uwierzytelnianie dwuskładnikowe",
|
||||
"Remove two-factor authentication?": "Usunąć uwierzytelnianie dwuskładnikowe?",
|
||||
"Require a verified email address": "Wymagaj zweryfikowanego adresu e-mail",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Uruchom je jako użytkownik, na którym działa Twój serwer WWW. Pełna procedura znajduje się w INSTALL.md.",
|
||||
"Score threshold": "Próg wyniku",
|
||||
"Search timezones…": "Szukaj stref czasowych…",
|
||||
"Security check failed. Please try again.": "Sprawdzanie zabezpieczeń nie powiodło się. Spróbuj ponownie.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Ta strona jest chroniona przez reCAPTCHA i obowiązują Polityka prywatności oraz Warunki korzystania z usługi Google.",
|
||||
"Those files have reached their download limit.": "Te pliki osiągnęły swój limit pobrań.",
|
||||
"Timezone": "Strefa czasowa",
|
||||
"To update, back up your database and files, then:": "Aby zaktualizować, wykonaj kopię zapasową bazy danych i plików, a następnie:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Aby zaktualizować, wykonaj kroki z INSTALL.md — najpierw kopia zapasowa, potem rozpakuj wydanie i uruchom migracje.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Wyłącz to tylko wtedy, gdy ten serwer nie powinien wykonywać żadnych żądań wychodzących. Jeśli usługa jest nieosiągalna, hasło i tak zostaje zaakceptowane, więc instalacja offline nigdy nie jest przez to blokowana.",
|
||||
"Two-factor authentication removed.": "Usunięto uwierzytelnianie dwuskładnikowe.",
|
||||
"Two-factor authentication removed by an admin": "Uwierzytelnianie dwuskładnikowe usunięte przez administratora",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Twoje konto jest ustawione na :timezone, więc to nie zmieni tego, jak daty wyglądają dla Ciebie.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Twój własny dzierżawca, nie „common”. Microsoft pozwala użytkownikowi zmienić adres e-mail konta, więc logowanie jest wiarygodne tylko wtedy, gdy token pochodzi od dzierżawcy podanego tutaj.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Twój dostawca odrzucił klucz tajny tej instalacji, więc chronione formularze są przepuszczane bez sprawdzania. Sprawdź klucz tajny poniżej.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Twoje publiczne formularze są chronione. W każdej chwili możesz przełączyć się na własne klucze."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Twoje publiczne formularze są chronione. W każdej chwili możesz przełączyć się na własne klucze.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Jeśli celowo wróciłeś do starszej wersji, uruchom php artisan projectsend:update, aby baza danych odpowiadała przywróconemu kodowi.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Pyta, zanim sprawdzi dostępność wydania, zanim je pobierze i zanim tknie Twoją instalację — a także weryfikuje sumę kontrolną tego, co pobiera. Pełna procedura znajduje się w UPDATE.md.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Nowe pliki są na miejscu, ale baza danych została doprowadzona tylko do :applied. Dopóki aktualizacja nie zostanie uruchomiona, ta instalacja działa na kodzie, do którego jej baza danych nie pasuje.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Okresowo sprawdza publiczne repozytorium projectsend.org pod kątem nowszego wydania i pokazuje informację na pulpicie. Nic nie jest nigdy pobierane ani instalowane automatycznie — aktualizację zawsze uruchamiasz sam, a skrypt aktualizacji pyta przed każdym krokiem.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Baza danych została zaktualizowana do :applied, a PHP nadal serwuje kod skompilowany wcześniej. Przeładowanie go usunie tę informację.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Baza danych została zaktualizowana do :applied, a ten kontener nadal działa na kodzie, z którym wystartował.",
|
||||
"This server is running version :running, but version :applied is installed": "Ten serwer działa na wersji :running, ale zainstalowana jest wersja :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Aby zaktualizować, uruchom sudo ./update.sh w katalogu instalacji — skrypt pyta, zanim cokolwiek pobierze lub zmieni.",
|
||||
"To update, run this in the install directory:": "Aby zaktualizować, uruchom to w katalogu instalacji:",
|
||||
"Version :running is installed, but the update has not been run": "Wersja :running jest zainstalowana, ale aktualizacja nie została uruchomiona",
|
||||
"Updated ProjectSend to :to, from :from": "Zaktualizował(a) ProjectSend do :to z :from",
|
||||
"ProjectSend was updated to a new version": "Zaktualizowano ProjectSend do nowej wersji",
|
||||
"Come and say hello": "Wpadnij się przywitać",
|
||||
"Continue to the dashboard": "Przejdź do pulpitu",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Dołącz do Discorda",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend ma Discorda: informacje o wydaniach, pomoc, gdy coś nie działa jak trzeba, i inne osoby korzystające z tego samego oprogramowania. My też tam jesteśmy.",
|
||||
"What's new": "Nowości",
|
||||
"What each release brought to this installation.": "Co każde wydanie wniosło do tej instalacji.",
|
||||
"Add the rest of your team": "Dodaj resztę zespołu",
|
||||
"Add your first client": "Dodaj pierwszego klienta",
|
||||
"Check the scheduler is running": "Sprawdź, czy harmonogram działa",
|
||||
"Choose how your email looks": "Wybierz wygląd swoich e-maili",
|
||||
"Choose how your file lists look": "Wybierz wygląd list plików",
|
||||
"Drop one in and choose who gets it.": "Wrzuć plik i wybierz, kto go dostanie.",
|
||||
"Essential": "Niezbędne",
|
||||
"Expiring files and queued email need it.": "Wygasające pliki i e-maile w kolejce tego potrzebują.",
|
||||
"Four layouts for the client-facing pages.": "Cztery układy stron, które widzą klienci.",
|
||||
"Getting started": "Pierwsze kroki",
|
||||
"Group your clients": "Pogrupuj swoich klientów",
|
||||
"Made with care in Argentina. Shared with the world.": "Zrobione z troską w Argentynie. Udostępnione światu.",
|
||||
"Previewed on a real message.": "Podgląd na prawdziwej wiadomości.",
|
||||
"Resets and share links go out through it.": "Resetowanie haseł i linki do udostępniania idą przez niego.",
|
||||
"Set up your mail server": "Skonfiguruj swój serwer poczty",
|
||||
"Share once instead of six times.": "Udostępnij raz zamiast sześć razy.",
|
||||
"Staff accounts, each scoped by its role.": "Konta zespołu, każde ograniczone swoją rolą.",
|
||||
"Thank you for installing ProjectSend": "Dziękujemy za zainstalowanie ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Dziękujemy za aktualizację ProjectSend",
|
||||
"The people you send files to.": "Osoby, którym wysyłasz pliki.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Krótka wersja tego, co zrobić na nowej instalacji. Nic z tego nie wygasa.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Masz teraz :version i wszystko znów działa. Dziękujemy, że nadal powierzasz swoje pliki ProjectSend.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Masz teraz :version, z :previous, i wszystko znów działa. Dziękujemy, że nadal powierzasz swoje pliki ProjectSend.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Używasz wersji :version i jest twoja. Oto, co warto zrobić najpierw.",
|
||||
"Thank you for choosing ProjectSend": "Dziękujemy za wybór ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Twoja witryna jest gotowa, a utrzymanie jej to nasza sprawa. Oto, co warto zrobić najpierw.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 zachowuje wszystko. Nieprzeczytane powiadomienia nigdy nie są usuwane, bez względu na to, jak stare są.",
|
||||
":version is available": "Dostępna jest wersja :version",
|
||||
":version on :date": ":version, :date",
|
||||
"Check now": "Sprawdź teraz",
|
||||
"Checked a moment ago, so this is the answer from then.": "Sprawdzenie nastąpiło przed chwilą, więc to odpowiedź z tamtego momentu.",
|
||||
"Checking…": "Sprawdzanie…",
|
||||
"Could not reach the update feed.": "Nie udało się połączyć z kanałem aktualizacji.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Zrzuć bazę danych przed jakąkolwiek zmianą i przerwij, jeśli zrzut się nie powiedzie.",
|
||||
"Housekeeping": "Porządki",
|
||||
"Keep failed jobs for": "Przechowuj nieudane zadania przez",
|
||||
"Keep read notifications for": "Przechowuj przeczytane powiadomienia przez",
|
||||
"Last checked :date": "Ostatnie sprawdzenie: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Nie ruszaj PHP ani workera — na wypadek, gdy restartujesz je samodzielnie.",
|
||||
"Other options": "Inne opcje",
|
||||
"Report which version is installed and which is available, and change nothing.": "Podaje, która wersja jest zainstalowana i która jest dostępna, i niczego nie zmienia.",
|
||||
"Retention updated.": "Okres przechowywania zaktualizowany.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Uruchomiony bez opcji skrypt pyta o każdą z tych rzeczy po kolei. Te są do uruchomień, przy których znasz już odpowiedź.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Pomija pytanie o kopię zapasową, dla uruchomienia, przy którym nie siedzisz.",
|
||||
"The latest release (:tag) is not a version this can compare.": "Najnowsze wydanie (:tag) nie jest wersją, którą można tu porównać.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Dwie rzeczy odkładają się same. To są okresy, do których stosują się codzienne czyszczenia powyżej.",
|
||||
"Up to date": "Aktualna",
|
||||
"Update checks are not available on this edition.": "Sprawdzanie aktualizacji nie jest dostępne w tej edycji.",
|
||||
"Update options": "Opcje aktualizacji",
|
||||
"Updated": "Zaktualizowano",
|
||||
"Version :version is available. You are running :current.": "Dostępna jest wersja :version. Używasz :current.",
|
||||
"You are up to date, running version :current.": "Masz najnowszą wersję :current.",
|
||||
"days": "dni",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help wypisuje wszystkie opcje, a UPDATE.md prowadzi przez całą procedurę, także ręczną.",
|
||||
"Fetch dashboard news": "Pobierz aktualności na pulpit",
|
||||
"Purge API request logs": "Wyczyść dziennik żądań API",
|
||||
"Purge erased accounts": "Wyczyść usunięte konta",
|
||||
"Purge expired files": "Wyczyść wygasłe pliki",
|
||||
"Purge failed jobs": "Wyczyść nieudane zadania",
|
||||
"Purge orphan files": "Wyczyść osierocone pliki",
|
||||
"Purge read notifications": "Wyczyść przeczytane powiadomienia",
|
||||
"Purge stale chunked uploads": "Wyczyść porzucone przesyłania częściowe",
|
||||
"Purge zip downloads": "Wyczyść pobrania ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Pasujących do tego filtra: :count, od najnowszych",
|
||||
"A file was previewed via the public group listing": "Wyświetlono podgląd pliku przez publiczną listę grup",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Każdy, kto ma link, może otworzyć obraz, wideo, plik audio lub PDF w przeglądarce zamiast go pobierać. Wyłącz tę opcję, jeśli pobieranie ma być jedynym sposobem obejrzenia pliku publicznego.",
|
||||
"Clients can preview files": "Klienci mogą wyświetlać podgląd plików",
|
||||
"Downloads & previews": "Pobrania i podglądy",
|
||||
"Downloads: :count": "Pobrania: :count",
|
||||
"Let visitors preview public files": "Zezwól odwiedzającym na podgląd plików publicznych",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Pozwala klientom otworzyć obraz, wideo, plik audio lub PDF w portalu, zamiast tylko go pobierać. Wyłącz tę opcję, jeśli pobieranie ma być jedynym sposobem obejrzenia pliku. Zespół zawsze ma dostęp do podglądu.",
|
||||
"No downloads match these filters.": "Żadne pobranie nie pasuje do tych filtrów.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Nikt jeszcze nie pobrał ani nie wyświetlił podglądu tego pliku.",
|
||||
"Preview :name": "Podgląd :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Wyświetlił(a) podgląd pliku \":subject\" przez publiczną listę grup",
|
||||
"Previews: :count": "Podglądy: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Ta instalacja sama buduje swoje obrazy, więc pobranie gotowego obrazu niczego nie zmienia. Jeśli wydanie zmieniło composer.lock lub frontend, uruchom także composer install i npm run build — pełną procedurę znajdziesz w UPDATE.md.",
|
||||
"To update, run this in the directory you cloned:": "Aby zaktualizować, uruchom to w sklonowanym katalogu:",
|
||||
"View all previews (:count)": "Pokaż wszystkie podglądy (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Twoja przeglądarka nie odtworzy tego pliku. Pobierz go, aby go obejrzeć."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count downloads",
|
||||
":count file deleted.|:count files deleted.": ":count arquivo excluído.|:count arquivos excluídos.",
|
||||
":count file imported.|:count files imported.": ":count arquivo importado.|:count arquivos importados.",
|
||||
":count files": ":count arquivos",
|
||||
":count folders": ":count pastas",
|
||||
":count items have been shared with you": ":count itens foram compartilhados com você",
|
||||
":count members": ":count membros",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(e mais :count erros)",
|
||||
"(and :count more errors)": "(e mais :count erros)|(e mais :count erros)|(e mais :count erros)",
|
||||
"(deleted account)": "(conta excluída)",
|
||||
"0 means unlimited.": "0 significa ilimitado.",
|
||||
"A client account request was approved": "Uma solicitação de conta de cliente foi aprovada",
|
||||
"A client account request was denied": "Uma solicitação de conta de cliente foi recusada",
|
||||
"A client left a group": "Um cliente saiu de um grupo",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Uma nova conta de cliente foi criada: :name (:email).",
|
||||
"A new client has registered": "Um novo cliente se cadastrou",
|
||||
"A new ProjectSend version is available": "Uma nova versão do ProjectSend está disponível",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "A página de um grupo público fica sempre acessível pelo próprio link assim que você o marca como público — esta seção controla apenas a URL base e um diretório opcional que lista de uma vez todos os grupos e arquivos públicos.",
|
||||
"A public link was created": "Um link público foi criado",
|
||||
"A public link was revoked": "Um link público foi revogado",
|
||||
"A role was created": "Uma função foi criada",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Administração: participação em grupo solicitada",
|
||||
"Admin: new client registered": "Administração: novo cliente cadastrado",
|
||||
"Administration": "Administração",
|
||||
"After uploading you can assign it to clients and groups": "Depois do envio, você poderá atribuí-lo a clientes e grupos",
|
||||
"Agree": "Concordar",
|
||||
"All": "Todos",
|
||||
"All actions": "Todas as ações",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Todos os comentários",
|
||||
"All failed jobs deleted.": "Todas as tarefas com falha foram excluídas.",
|
||||
"All files": "Todos os arquivos",
|
||||
"All groups": "Todos os grupos",
|
||||
"All permissions": "Todas as permissões",
|
||||
"All rights reserved.": "Todos os direitos reservados.",
|
||||
"All roles": "Todas as funções",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Quem estiver usando não conseguirá mais baixar o arquivo.",
|
||||
"Appearance": "Aparência",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Aplica-se às páginas públicas de grupos e arquivos voltadas a visitantes e ao portal do cliente \"Meus arquivos\".",
|
||||
"Apply filters": "Aplicar filtros",
|
||||
"Approve": "Aprovar",
|
||||
"Approve account requests": "Aprovar solicitações de conta",
|
||||
"Approve group membership requests": "Aprovar solicitações de participação em grupos",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Tem certeza de que deseja excluir sua conta?",
|
||||
"Assign": "Atribuir",
|
||||
"Assign categories to files": "Atribuir categorias a arquivos",
|
||||
"Assigned": "Atribuído",
|
||||
"Assigned clients": "Clientes atribuídos",
|
||||
"Assigned the file \":subject\" to :target": "Atribuiu o arquivo \":subject\" a :target",
|
||||
"Associate": "Associado",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Autor",
|
||||
"Available groups": "Grupos disponíveis",
|
||||
"Available placeholders": "Marcadores disponíveis",
|
||||
"Back to My files": "Voltar para Meus arquivos",
|
||||
"Bad Gateway": "Gateway Inválido",
|
||||
"Bad Request": "Requisição Inválida",
|
||||
"Bandwidth Limit Exceeded": "Limite de Banda Excedido",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Corpo da mensagem",
|
||||
"Browse": "Navegar",
|
||||
"Browse publicly shared groups and files below.": "Navegue abaixo pelos grupos e arquivos compartilhados publicamente.",
|
||||
"Built-in": "Integrada",
|
||||
"Built-in role names cannot be changed.": "O nome das funções integradas não pode ser alterado.",
|
||||
"Built-in roles cannot be deleted.": "As funções integradas não podem ser excluídas.",
|
||||
"Call to action": "Chamada para ação",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Confirmar senha",
|
||||
"Conflict": "Conflito",
|
||||
"Connect": "Conectar",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Conecte um bucket externo — compatível com S3 (AWS S3, MinIO, Backblaze) ou Google Cloud Storage — como armazenamento dos novos envios.",
|
||||
"Connection Closed Without Response": "Conexão Fechada Sem Resposta",
|
||||
"Connection Timed Out": "Tempo Limite Da Conexão",
|
||||
"Continue": "Continuar",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Desative sua conta agora; ela é apagada definitivamente após :days dias",
|
||||
"Deactivated the account \":subject\"": "Desativou a conta \":subject\"",
|
||||
"Default": "Padrão",
|
||||
"Defaults to the file name.": "Por padrão, usa o nome do arquivo.",
|
||||
"Delete": "Excluir",
|
||||
"Delete :name": "Excluir :name",
|
||||
"Delete account": "Excluir conta",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Recusar",
|
||||
"Deny this request?": "Recusar esta solicitação?",
|
||||
"Description": "Descrição",
|
||||
"Description (optional)": "Descrição (opcional)",
|
||||
"Detach": "Desanexar",
|
||||
"Details": "Detalhes",
|
||||
"Disable": "Desativar",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Ir para a página :page",
|
||||
"Go to the login form": "Ir para o formulário de acesso",
|
||||
"Gone": "Perdido",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Cinza",
|
||||
"Green": "Verde",
|
||||
"Group": "Grupo",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Iniciar Sessão",
|
||||
"Logout": "Encerrar Sessão",
|
||||
"Loop Detected": "Loop Detectado",
|
||||
"Mail provider settings saved.": "Configurações do provedor de e-mail salvas.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Modo De Manutenção",
|
||||
"Manage custom fields": "Gerenciar campos personalizados",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Nenhum arquivo foi compartilhado com você ainda.",
|
||||
"No files here yet.": "Ainda não há arquivos aqui.",
|
||||
"No files or folders match your search.": "Nenhum arquivo ou pasta corresponde à sua busca.",
|
||||
"No files yet. Upload the first one.": "Ainda não há arquivos. Envie o primeiro.",
|
||||
"No folder": "Sem pasta",
|
||||
"No groups match these filters.": "Nenhum grupo corresponde a estes filtros.",
|
||||
"No groups yet.": "Ainda não há grupos.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Ainda não há destinatários adicionados.",
|
||||
"No requests match your search.": "Nenhuma solicitação corresponde à sua busca.",
|
||||
"No roles match this filter.": "Nenhuma função corresponde a este filtro.",
|
||||
"No service account key has been saved yet.": "Nenhuma chave de conta de serviço foi salva ainda.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Sem limite de tamanho. Os envios podem ser pausados e retomam automaticamente após interrupções.",
|
||||
"No users match these filters.": "Nenhum usuário corresponde a estes filtros.",
|
||||
"No users yet.": "Ainda não há usuários.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Laranja",
|
||||
"Origin Is Unreachable": "Origem É Inacessível",
|
||||
"Outgoing email": "E-mails enviados",
|
||||
"Outgoing email theme": "Tema dos e-mails enviados",
|
||||
"Page :current of :last": "Página :current de :last",
|
||||
"Page Expired": "Página expirada",
|
||||
"Pagination Navigation": "Navegação em resultados paginados",
|
||||
"Partial Content": "Conteúdo Parcial",
|
||||
"Password": "Senha",
|
||||
"Password reset": "Redefinição de senha",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Cole o arquivo de chave JSON de uma conta de serviço com acesso de leitura e escrita aos objetos do bucket. Ele é armazenado criptografado e nunca mais é exibido.",
|
||||
"Path": "Caminho",
|
||||
"Payload Too Large": "Carga Muito Grande",
|
||||
"Payment Required": "Pagamento Requerido",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Configurações da listagem pública",
|
||||
"Public listing URL": "URL da listagem pública",
|
||||
"Public pages & portal": "Páginas públicas e portal",
|
||||
"Public pages and client portal theme": "Tema das páginas públicas e do portal do cliente",
|
||||
"Published": "Publicado",
|
||||
"Purple": "Roxo",
|
||||
"Quota": "Cota",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Restaurar",
|
||||
"Restore :name": "Restaurar :name",
|
||||
"Restricted extension": "Extensão restrita",
|
||||
"Result": "Resultado",
|
||||
"results": "resultados",
|
||||
"Review membership requests": "Analisar solicitações de participação",
|
||||
"Review pending requests": "Analisar solicitações pendentes",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Nome da função",
|
||||
"Role updated.": "Função atualizada.",
|
||||
"Roles": "Funções",
|
||||
"S3-compatible": "Compatível com S3",
|
||||
"Sample email": "E-mail de exemplo",
|
||||
"Save": "Salvar",
|
||||
"Save :name": "Economize :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Envia um e-mail de verdade agora mesmo, sem passar pela fila, para você conferir se as configurações acima realmente funcionam.",
|
||||
"Separate paragraphs with a blank line.": "Separe os parágrafos com uma linha em branco.",
|
||||
"Server Error": "Erro do servidor",
|
||||
"Service account key": "Chave de conta de serviço",
|
||||
"Service Unavailable": "Serviço indisponível",
|
||||
"Session Has Expired": "Sessão Expirou",
|
||||
"Set file expiration dates": "Definir datas de validade dos arquivos",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Configurações",
|
||||
"Settings that apply to the whole installation": "Configurações que valem para toda a instalação",
|
||||
"Setup complete": "Configuração concluída",
|
||||
"Share a file with the people who manage your account.": "Compartilhe um arquivo com as pessoas que cuidam da sua conta.",
|
||||
"Share files with several clients at once": "Compartilhe arquivos com vários clientes de uma vez",
|
||||
"Shared the folder \":subject\" with :target": "Compartilhou a pasta \":subject\" com :target",
|
||||
"Shared with": "Compartilhado com",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Mudando Protocolos",
|
||||
"System": "Sistema",
|
||||
"System Administrator": "Administrador do sistema",
|
||||
"System settings": "Configurações do sistema",
|
||||
"System settings were updated": "As configurações do sistema foram atualizadas",
|
||||
"System users": "Usuários do sistema",
|
||||
"Tag": "Marcação",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Teste",
|
||||
"Text": "Texto",
|
||||
"Text or author": "Texto ou autor",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Isso não parece um arquivo de chave de conta de serviço: não é um JSON válido.",
|
||||
"That service account key file is missing its :field.": "Falta :field nesse arquivo de chave de conta de serviço.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "A conta de :name será excluída definitivamente. Não é possível desfazer.",
|
||||
"The account password was changed": "A senha da conta foi alterada",
|
||||
"The account request of :name will be denied and the account deleted.": "A solicitação de conta de :name será recusada e a conta excluída.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Os grupos dos quais você faz parte e aqueles em que pode pedir para entrar",
|
||||
"The header/footer style used for every outgoing notification email.": "O estilo de cabeçalho e rodapé usado em todo e-mail de notificação enviado.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "A aparência das páginas públicas para visitantes, do portal do cliente e dos e-mails de notificação enviados.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "O servidor de e-mail em si é configurado no arquivo .env desta instalação (variáveis MAIL_*), não aqui.",
|
||||
"The people who administer this installation and upload files": "As pessoas que administram esta instalação e enviam arquivos",
|
||||
"The people you share files with": "As pessoas com quem você compartilha arquivos",
|
||||
"The provided two-factor authentication code was invalid.": "O código de autenticação em duas etapas informado é inválido.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Ainda não há nada para configurar.",
|
||||
"Test email from :site": "E-mail de teste de :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Estes arquivos serão removidos definitivamente do disco. Não é possível desfazer.",
|
||||
"Theme": "Tema",
|
||||
"Theme updated.": "Tema atualizado.",
|
||||
"Theming": "Temas",
|
||||
"Theming settings": "Configurações de temas",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Este envio excederia sua cota de armazenamento de :quota MB.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Dica: arraste arquivos e pastas para uma pasta ou para a trilha de navegação para movê-los.",
|
||||
"Title": "Título",
|
||||
"Title (optional)": "Título (opcional)",
|
||||
"TLS": "TLS",
|
||||
"to": "até",
|
||||
"To": "Para",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Arquivos demais selecionados. Reduza a seleção e tente novamente.",
|
||||
"Too Many Requests": "Muitas solicitações",
|
||||
"Transactional email settings for this installation": "Configurações de e-mails transacionais desta instalação",
|
||||
"Transfers — last 30 days": "Transferências — últimos 30 dias",
|
||||
"Translate": "Traduzir",
|
||||
"Translate It": "Traduza isso",
|
||||
"Turns all transactional email for this installation on or off.": "Liga ou desliga todos os e-mails transacionais desta instalação.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Enviou o arquivo \":subject\"",
|
||||
"Uploader": "Autor do envio",
|
||||
"Uploads": "Envios",
|
||||
"Uploads (chart)": "Envios (gráfico)",
|
||||
"URI Too Long": "URI Muito Grande",
|
||||
"URL slug": "Slug da URL",
|
||||
"Use a custom link instead of a random one": "Usar um link personalizado em vez de um aleatório",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Esta é a única vez em que ele será exibido. Guardamos apenas um hash, então não é possível recuperá-lo depois.",
|
||||
"Copy to clipboard": "Copiar para a área de transferência",
|
||||
"Copied": "Copiado",
|
||||
"Your tokens": "Seus tokens",
|
||||
"Revoking a token takes effect immediately.": "A revogação de um token tem efeito imediato.",
|
||||
"You have not created any tokens yet.": "Você ainda não criou nenhum token.",
|
||||
"Expired": "Expirado",
|
||||
"Never used": "Nunca usado",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Gerenciar campos personalizados de clientes",
|
||||
"Moderate comments": "Moderar comentários",
|
||||
"Only me": "Somente eu",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Visível para qualquer pessoa que possa abrir este arquivo, inclusive visitantes não conectados.",
|
||||
"Anyone, including visitors who are not logged in": "Qualquer pessoa, inclusive visitantes não conectados",
|
||||
"Staff and clients": "Equipe e clientes",
|
||||
"Staff only": "Somente a equipe",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Os comentários já escritos continuam legíveis mesmo que o arquivo deixe de aceitar novos.",
|
||||
"Comments awaiting approval": "Comentários aguardando aprovação",
|
||||
"Comment settings": "Configurações de comentários",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Comentários deixados por visitantes não conectados. Ninguém consegue vê-los até que você os aprove.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Conecte um bucket externo compatível com S3 (AWS S3, MinIO ou outro serviço compatível) como armazenamento dos novos envios.",
|
||||
"Custom endpoint": "Endpoint personalizado",
|
||||
"Custom field created.": "Campo personalizado criado.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Nenhuma tarefa com falha.",
|
||||
"No files match these filters.": "Nenhum arquivo corresponde a estes filtros.",
|
||||
"None of the selected files could be edited.": "Nenhum dos arquivos selecionados pôde ser editado.",
|
||||
"Nothing is waiting for approval.": "Não há nada aguardando aprovação.",
|
||||
"of :count": "de :count",
|
||||
"of :quota": "de :quota",
|
||||
"on": "em",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Página",
|
||||
"Path prefix": "Prefixo do caminho",
|
||||
"Pending": "Pendente",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Verifica periodicamente se o repositório público do projectsend.org tem uma versão mais nova e mostra um aviso no painel. Nada é baixado ou aplicado automaticamente.",
|
||||
"Permanently failed": "Falhou definitivamente",
|
||||
"Post comment": "Publicar comentário",
|
||||
"Preview of :name": "Prévia de :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Remover a verificação em duas etapas",
|
||||
"Remove two-factor authentication?": "Remover a verificação em duas etapas?",
|
||||
"Require a verified email address": "Exigir um endereço de e-mail verificado",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Execute isto como o usuário com o qual seu servidor web roda. O INSTALL.md tem o procedimento completo.",
|
||||
"Score threshold": "Limite de pontuação",
|
||||
"Search timezones…": "Buscar fusos horários…",
|
||||
"Security check failed. Please try again.": "A verificação de segurança falhou. Tente de novo.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Este site é protegido pelo reCAPTCHA e aplicam-se a Política de Privacidade e os Termos de Serviço do Google.",
|
||||
"Those files have reached their download limit.": "Esses arquivos atingiram seu limite de downloads.",
|
||||
"Timezone": "Fuso horário",
|
||||
"To update, back up your database and files, then:": "Para atualizar, faça backup do seu banco de dados e dos seus arquivos e depois:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Para atualizar, siga os passos do INSTALL.md — faça backup primeiro, depois descompacte a versão e rode as migrações.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Desative isto só se este servidor não deve fazer requisições externas. Se o serviço não puder ser contatado, a senha é aceita mesmo assim, então uma instalação sem internet nunca fica bloqueada por isto.",
|
||||
"Two-factor authentication removed.": "Verificação em duas etapas removida.",
|
||||
"Two-factor authentication removed by an admin": "Verificação em duas etapas removida por um administrador",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Sua própria conta está em :timezone, então isto não vai mudar como as datas aparecem para você.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Seu próprio locatário, não \"common\". A Microsoft permite que um usuário mude o endereço de e-mail da conta, então um acesso só é confiável quando o token veio do locatário que você indicou aqui.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Seu provedor rejeitou a chave secreta desta instalação, então os formulários protegidos estão passando sem checagem. Confira a chave secreta abaixo.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Seus formulários públicos estão protegidos. Você pode mudar para as suas próprias chaves a qualquer momento."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Seus formulários públicos estão protegidos. Você pode mudar para as suas próprias chaves a qualquer momento.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Se você voltou a uma versão anterior de propósito, rode php artisan projectsend:update para que o banco de dados corresponda ao código que você restaurou.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Ele pergunta antes de procurar uma versão, antes de baixar e antes de mexer na sua instalação — e verifica a soma de verificação do que baixa. O UPDATE.md tem o procedimento completo.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Os arquivos novos já estão no lugar, mas o banco de dados só foi atualizado até :applied. Enquanto a atualização não rodar, esta instalação está executando um código que não corresponde ao seu banco de dados.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Verifica periodicamente se o repositório público do projectsend.org tem uma versão mais nova e mostra um aviso no painel. Nada é baixado ou aplicado automaticamente: atualizar é sempre algo que você executa, e o script de atualização pergunta antes de cada passo.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "O banco de dados foi atualizado para :applied, e o PHP continua servindo o código que ele tinha compilado antes disso. Recarregá-lo faz este aviso sumir.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "O banco de dados foi atualizado para :applied, e este contêiner continua rodando o código com que subiu.",
|
||||
"This server is running version :running, but version :applied is installed": "Este servidor está rodando a versão :running, mas a versão instalada é :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Para atualizar, rode sudo ./update.sh no diretório de instalação — ele pergunta antes de baixar ou mudar qualquer coisa.",
|
||||
"To update, run this in the install directory:": "Para atualizar, rode isto no diretório de instalação:",
|
||||
"Version :running is installed, but the update has not been run": "A versão :running está instalada, mas a atualização não foi executada",
|
||||
"Updated ProjectSend to :to, from :from": "Atualizou o ProjectSend para :to, a partir de :from",
|
||||
"ProjectSend was updated to a new version": "O ProjectSend foi atualizado para uma nova versão",
|
||||
"Come and say hello": "Venha dizer olá",
|
||||
"Continue to the dashboard": "Continuar para o painel",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Entrar no Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "O ProjectSend tem um Discord: novidades de cada versão, ajuda quando algo não está se comportando como deveria e outras pessoas que usam o mesmo software. Nós também estamos lá.",
|
||||
"What's new": "Novidades",
|
||||
"What each release brought to this installation.": "O que cada versão trouxe para esta instalação.",
|
||||
"Add the rest of your team": "Adicione o resto da sua equipe",
|
||||
"Add your first client": "Adicione seu primeiro cliente",
|
||||
"Check the scheduler is running": "Confira se o agendador está rodando",
|
||||
"Choose how your email looks": "Escolha como seus e-mails aparecem",
|
||||
"Choose how your file lists look": "Escolha como suas listas de arquivos aparecem",
|
||||
"Drop one in and choose who gets it.": "Solte um aqui e escolha quem recebe.",
|
||||
"Essential": "Essencial",
|
||||
"Expiring files and queued email need it.": "Arquivos que expiram e e-mails na fila dependem dele.",
|
||||
"Four layouts for the client-facing pages.": "Quatro layouts para as páginas que os clientes veem.",
|
||||
"Getting started": "Primeiros passos",
|
||||
"Group your clients": "Agrupe seus clientes",
|
||||
"Made with care in Argentina. Shared with the world.": "Feito com carinho na Argentina. Compartilhado com o mundo.",
|
||||
"Previewed on a real message.": "Pré-visualizado em uma mensagem real.",
|
||||
"Resets and share links go out through it.": "Redefinições de senha e links de compartilhamento saem por ele.",
|
||||
"Set up your mail server": "Configure seu servidor de e-mail",
|
||||
"Share once instead of six times.": "Compartilhe uma vez em vez de seis.",
|
||||
"Staff accounts, each scoped by its role.": "Contas da equipe, cada uma limitada pela sua função.",
|
||||
"Thank you for installing ProjectSend": "Obrigado por instalar o ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Obrigado por atualizar o ProjectSend",
|
||||
"The people you send files to.": "As pessoas para quem você envia arquivos.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "A versão curta do que fazer em uma instalação nova. Nada disso expira.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Você está na :version agora e tudo voltou a funcionar. Obrigado por continuar confiando seus arquivos ao ProjectSend.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Você está na :version agora, vindo da :previous, e tudo voltou a funcionar. Obrigado por continuar confiando seus arquivos ao ProjectSend.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Você está usando a versão :version, e ela é sua. Veja o que vale a pena fazer primeiro.",
|
||||
"Thank you for choosing ProjectSend": "Obrigado por escolher o ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Seu site está pronto, e mantê-lo funcionando é trabalho nosso. Veja o que vale a pena fazer primeiro.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 mantém tudo. Notificações não lidas nunca são excluídas, por mais antigas que sejam.",
|
||||
":version is available": ":version está disponível",
|
||||
":version on :date": ":version em :date",
|
||||
"Check now": "Verificar agora",
|
||||
"Checked a moment ago, so this is the answer from then.": "A verificação foi há pouco, então esta é a resposta daquele momento.",
|
||||
"Checking…": "Verificando…",
|
||||
"Could not reach the update feed.": "Não foi possível acessar o feed de atualizações.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Faça um dump do banco de dados antes de mexer em qualquer coisa, e pare se o dump falhar.",
|
||||
"Housekeeping": "Manutenção",
|
||||
"Keep failed jobs for": "Manter tarefas com falha por",
|
||||
"Keep read notifications for": "Manter notificações lidas por",
|
||||
"Last checked :date": "Última verificação: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Não mexer no PHP nem no worker, para quando você mesmo os reiniciar.",
|
||||
"Other options": "Outras opções",
|
||||
"Report which version is installed and which is available, and change nothing.": "Informa qual versão está instalada e qual está disponível, sem mudar nada.",
|
||||
"Retention updated.": "Retenção atualizada.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Se você executar o script sem opções, ele pergunta cada uma dessas coisas no caminho. Estas são para as execuções em que você já sabe a resposta.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Pula a pergunta sobre backup, para uma execução que você não está acompanhando.",
|
||||
"The latest release (:tag) is not a version this can compare.": "A versão mais recente (:tag) não é uma versão que dê para comparar aqui.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Duas coisas se acumulam sozinhas. Estes são os prazos que as limpezas diárias acima seguem.",
|
||||
"Up to date": "Em dia",
|
||||
"Update checks are not available on this edition.": "As verificações de atualização não estão disponíveis nesta edição.",
|
||||
"Update options": "Opções de atualização",
|
||||
"Updated": "Atualizado",
|
||||
"Version :version is available. You are running :current.": "A versão :version está disponível. Você está usando a :current.",
|
||||
"You are up to date, running version :current.": "Você está em dia, na versão :current.",
|
||||
"days": "dias",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help lista todas as opções, e UPDATE.md descreve todo o procedimento, inclusive fazê-lo à mão.",
|
||||
"Fetch dashboard news": "Buscar novidades do painel",
|
||||
"Purge API request logs": "Limpar o registro de requisições da API",
|
||||
"Purge erased accounts": "Limpar contas apagadas",
|
||||
"Purge expired files": "Limpar arquivos expirados",
|
||||
"Purge failed jobs": "Limpar tarefas com falha",
|
||||
"Purge orphan files": "Limpar arquivos órfãos",
|
||||
"Purge read notifications": "Limpar notificações lidas",
|
||||
"Purge stale chunked uploads": "Limpar envios em partes abandonados",
|
||||
"Purge zip downloads": "Limpar downloads ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Correspondem a este filtro: :count, da mais recente à mais antiga",
|
||||
"A file was previewed via the public group listing": "Um arquivo foi pré-visualizado pela listagem pública de grupos",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Qualquer pessoa com o link pode abrir uma imagem, um vídeo, um arquivo de áudio ou um PDF no navegador em vez de baixá-lo. Desative para que baixar seja a única forma de ver um arquivo público.",
|
||||
"Clients can preview files": "Os clientes podem pré-visualizar arquivos",
|
||||
"Downloads & previews": "Downloads e pré-visualizações",
|
||||
"Downloads: :count": "Downloads: :count",
|
||||
"Let visitors preview public files": "Permitir que visitantes pré-visualizem arquivos públicos",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Permite que os clientes abram uma imagem, um vídeo, um arquivo de áudio ou um PDF no portal em vez de apenas baixá-lo. Desative para que baixar seja a única forma de ver um arquivo. A equipe sempre pode pré-visualizar.",
|
||||
"No downloads match these filters.": "Nenhum download corresponde a estes filtros.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Ninguém baixou nem pré-visualizou este arquivo ainda.",
|
||||
"Preview :name": "Pré-visualizar :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Pré-visualizou o arquivo \":subject\" pela listagem pública de grupos",
|
||||
"Previews: :count": "Pré-visualizações: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Esta instalação constrói as próprias imagens, então baixar uma não muda nada. Se a versão mexeu no composer.lock ou no frontend, rode também composer install e npm run build — o UPDATE.md tem o procedimento completo.",
|
||||
"To update, run this in the directory you cloned:": "Para atualizar, rode isto no diretório que você clonou:",
|
||||
"View all previews (:count)": "Ver todas as pré-visualizações (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Seu navegador não consegue reproduzir este arquivo. Baixe o arquivo para visualizá-lo."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": "Скачиваний: :count",
|
||||
":count file deleted.|:count files deleted.": "Удалён :count файл.|Удалено файлов: :count.",
|
||||
":count file imported.|:count files imported.": "Импортирован :count файл.|Импортировано файлов: :count.",
|
||||
":count files": "Файлов: :count",
|
||||
":count folders": "Папок: :count",
|
||||
":count items have been shared with you": "Вам открыт доступ к элементам: :count",
|
||||
":count members": "Участников: :count",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(и ещё :count ошибка)",
|
||||
"(and :count more errors)": "(и ещё :count ошибка)|(и ещё :count ошибки)|(и ещё :count ошибок)",
|
||||
"(deleted account)": "(удалённая учётная запись)",
|
||||
"0 means unlimited.": "0 означает без ограничений.",
|
||||
"A client account request was approved": "Заявка на учётную запись клиента одобрена",
|
||||
"A client account request was denied": "Заявка на учётную запись клиента отклонена",
|
||||
"A client left a group": "Клиент покинул группу",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Создана новая учётная запись клиента: :name (:email).",
|
||||
"A new client has registered": "Зарегистрировался новый клиент",
|
||||
"A new ProjectSend version is available": "Доступна новая версия ProjectSend",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Страница публичной группы всегда доступна по собственной ссылке, как только вы отметите её как публичную, — этот раздел управляет только базовым адресом и необязательным каталогом, который выводит сразу все публичные группы и файлы.",
|
||||
"A public link was created": "Создана публичная ссылка",
|
||||
"A public link was revoked": "Публичная ссылка отозвана",
|
||||
"A role was created": "Роль создана",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Администрирование: подана заявка на членство в группе",
|
||||
"Admin: new client registered": "Администрирование: зарегистрировался новый клиент",
|
||||
"Administration": "Администрирование",
|
||||
"After uploading you can assign it to clients and groups": "После загрузки вы сможете назначить его клиентам и группам",
|
||||
"Agree": "Согласен(на)",
|
||||
"All": "Все",
|
||||
"All actions": "Все действия",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Все комментарии",
|
||||
"All failed jobs deleted.": "Все неудавшиеся задачи удалены.",
|
||||
"All files": "Все файлы",
|
||||
"All groups": "Все группы",
|
||||
"All permissions": "Все разрешения",
|
||||
"All rights reserved.": "Все права защищены.",
|
||||
"All roles": "Все роли",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Тот, кто ею пользуется, больше не сможет скачать файл.",
|
||||
"Appearance": "Оформление",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Относится к публичным страницам групп и файлов для посетителей и к клиентскому порталу «Мои файлы».",
|
||||
"Apply filters": "Применить фильтры",
|
||||
"Approve": "Одобрить",
|
||||
"Approve account requests": "Одобрение заявок на учётные записи",
|
||||
"Approve group membership requests": "Одобрение заявок на членство в группах",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Вы уверены, что хотите удалить свою учётную запись?",
|
||||
"Assign": "Назначить",
|
||||
"Assign categories to files": "Назначение категорий файлам",
|
||||
"Assigned": "Назначено",
|
||||
"Assigned clients": "Назначенные клиенты",
|
||||
"Assigned the file \":subject\" to :target": "Назначил(а) файл «:subject» для :target",
|
||||
"Associate": "Ассоциировать",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Автор",
|
||||
"Available groups": "Доступные группы",
|
||||
"Available placeholders": "Доступные подстановки",
|
||||
"Back to My files": "Назад к «Моим файлам»",
|
||||
"Bad Gateway": "Проблема с шлюзом",
|
||||
"Bad Request": "Некорректный запрос",
|
||||
"Bandwidth Limit Exceeded": "Превышена нагрузка на канал связи",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Текст сообщения",
|
||||
"Browse": "Просмотр",
|
||||
"Browse publicly shared groups and files below.": "Ниже вы можете просмотреть публично доступные группы и файлы.",
|
||||
"Built-in": "Встроенная",
|
||||
"Built-in role names cannot be changed.": "Названия встроенных ролей изменить нельзя.",
|
||||
"Built-in roles cannot be deleted.": "Встроенные роли удалить нельзя.",
|
||||
"Call to action": "Призыв к действию",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Подтверждение пароля",
|
||||
"Conflict": "Конфликт",
|
||||
"Connect": "Подключить",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Подключите внешний бакет — S3-совместимый (AWS S3, MinIO, Backblaze) или Google Cloud Storage — как хранилище для новых загрузок.",
|
||||
"Connection Closed Without Response": "Соединение закрыто без ответа",
|
||||
"Connection Timed Out": "Соединение не отвечает",
|
||||
"Continue": "Продолжить",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Отключите свою учётную запись сейчас; через :days дн. она будет удалена безвозвратно",
|
||||
"Deactivated the account \":subject\"": "Отключил(а) учётную запись «:subject»",
|
||||
"Default": "По умолчанию",
|
||||
"Defaults to the file name.": "По умолчанию — имя файла.",
|
||||
"Delete": "Удалить",
|
||||
"Delete :name": "Удалить :name",
|
||||
"Delete account": "Удалить учётную запись",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Отклонить",
|
||||
"Deny this request?": "Отклонить эту заявку?",
|
||||
"Description": "Описание",
|
||||
"Description (optional)": "Описание (необязательно)",
|
||||
"Detach": "Открепить",
|
||||
"Details": "Подробности",
|
||||
"Disable": "Отключить",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Перейти к :page-й странице",
|
||||
"Go to the login form": "Перейти к форме входа",
|
||||
"Gone": "Удалено",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Серый",
|
||||
"Green": "Зелёный",
|
||||
"Group": "Группа",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Войти",
|
||||
"Logout": "Выйти",
|
||||
"Loop Detected": "Обнаружен бесконечный цикл",
|
||||
"Mail provider settings saved.": "Настройки почтового провайдера сохранены.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Ведутся технические работы",
|
||||
"Manage custom fields": "Управление дополнительными полями",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Вам пока не открыли доступ ни к одному файлу.",
|
||||
"No files here yet.": "Здесь пока нет файлов.",
|
||||
"No files or folders match your search.": "Нет файлов или папок, подходящих под ваш запрос.",
|
||||
"No files yet. Upload the first one.": "Файлов пока нет. Загрузите первый.",
|
||||
"No folder": "Без папки",
|
||||
"No groups match these filters.": "Нет групп, подходящих под эти фильтры.",
|
||||
"No groups yet.": "Групп пока нет.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Получатели пока не добавлены.",
|
||||
"No requests match your search.": "Нет заявок, подходящих под ваш запрос.",
|
||||
"No roles match this filter.": "Нет ролей, подходящих под этот фильтр.",
|
||||
"No service account key has been saved yet.": "Ключ сервисного аккаунта ещё не сохранён.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Без ограничения размера. Загрузку можно приостановить, после обрыва она продолжится сама.",
|
||||
"No users match these filters.": "Нет пользователей, подходящих под эти фильтры.",
|
||||
"No users yet.": "Пользователей пока нет.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Оранжевый",
|
||||
"Origin Is Unreachable": "Источник недоступен",
|
||||
"Outgoing email": "Исходящая почта",
|
||||
"Outgoing email theme": "Тема исходящих писем",
|
||||
"Page :current of :last": "Страница :current из :last",
|
||||
"Page Expired": "Страница устарела",
|
||||
"Pagination Navigation": "Навигация",
|
||||
"Partial Content": "Не полное содержимое",
|
||||
"Password": "Пароль",
|
||||
"Password reset": "Сброс пароля",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Вставьте JSON-файл ключа сервисного аккаунта с правами на чтение и запись объектов в бакете. Он хранится в зашифрованном виде и больше не показывается.",
|
||||
"Path": "Путь",
|
||||
"Payload Too Large": "Большой объём данных",
|
||||
"Payment Required": "Необходима оплата",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Настройки публичного списка",
|
||||
"Public listing URL": "Адрес публичного списка",
|
||||
"Public pages & portal": "Публичные страницы и портал",
|
||||
"Public pages and client portal theme": "Тема публичных страниц и клиентского портала",
|
||||
"Published": "Опубликовано",
|
||||
"Purple": "Фиолетовый",
|
||||
"Quota": "Квота",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Восстановить",
|
||||
"Restore :name": "Восстановить :name",
|
||||
"Restricted extension": "Ограниченное расширение",
|
||||
"Result": "Результат",
|
||||
"results": "результатов",
|
||||
"Review membership requests": "Рассмотрение заявок на членство",
|
||||
"Review pending requests": "Рассмотреть ожидающие заявки",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Название роли",
|
||||
"Role updated.": "Роль обновлена.",
|
||||
"Roles": "Роли",
|
||||
"S3-compatible": "S3-совместимое",
|
||||
"Sample email": "Пример письма",
|
||||
"Save": "Сохранить",
|
||||
"Save :name": "Сохранить :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Отправляет настоящее письмо прямо сейчас, минуя очередь, чтобы вы убедились, что настройки выше действительно работают.",
|
||||
"Separate paragraphs with a blank line.": "Разделяйте абзацы пустой строкой.",
|
||||
"Server Error": "Ошибка сервера",
|
||||
"Service account key": "Ключ сервисного аккаунта",
|
||||
"Service Unavailable": "Сервис недоступен",
|
||||
"Session Has Expired": "Сессия устарела",
|
||||
"Set file expiration dates": "Установка сроков действия файлов",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Настройки",
|
||||
"Settings that apply to the whole installation": "Настройки, действующие на всю установку",
|
||||
"Setup complete": "Настройка завершена",
|
||||
"Share a file with the people who manage your account.": "Поделитесь файлом с теми, кто ведёт вашу учётную запись.",
|
||||
"Share files with several clients at once": "Открывайте доступ к файлам сразу нескольким клиентам",
|
||||
"Shared the folder \":subject\" with :target": "Открыл(а) доступ к папке «:subject» для :target",
|
||||
"Shared with": "Доступ открыт",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Переключение протоколов",
|
||||
"System": "Система",
|
||||
"System Administrator": "Системный администратор",
|
||||
"System settings": "Системные настройки",
|
||||
"System settings were updated": "Системные настройки обновлены",
|
||||
"System users": "Пользователи системы",
|
||||
"Tag": "Тег",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Проверка",
|
||||
"Text": "Текст",
|
||||
"Text or author": "Текст или автор",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Это не похоже на файл ключа сервисного аккаунта: это не корректный JSON.",
|
||||
"That service account key file is missing its :field.": "В этом файле ключа сервисного аккаунта отсутствует :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Учётная запись :name будет удалена безвозвратно. Отменить это нельзя.",
|
||||
"The account password was changed": "Пароль учётной записи изменён",
|
||||
"The account request of :name will be denied and the account deleted.": "Заявка :name на учётную запись будет отклонена, а сама запись удалена.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Группы, в которых вы состоите, и те, куда вы можете попроситься",
|
||||
"The header/footer style used for every outgoing notification email.": "Стиль шапки и подвала для каждого исходящего письма с уведомлением.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Внешний вид публичных страниц для посетителей, клиентского портала и исходящих писем с уведомлениями.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Сам почтовый сервер настраивается в файле .env этой установки (переменные MAIL_*), а не здесь.",
|
||||
"The people who administer this installation and upload files": "Люди, которые администрируют эту установку и загружают файлы",
|
||||
"The people you share files with": "Люди, с которыми вы делитесь файлами",
|
||||
"The provided two-factor authentication code was invalid.": "Введённый код двухфакторной аутентификации неверен.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Настраивать пока нечего.",
|
||||
"Test email from :site": "Пробное письмо с :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Эти файлы будут безвозвратно удалены с диска. Отменить это нельзя.",
|
||||
"Theme": "Тема оформления",
|
||||
"Theme updated.": "Тема оформления обновлена.",
|
||||
"Theming": "Оформление",
|
||||
"Theming settings": "Настройки оформления",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Эта загрузка превысила бы вашу квоту хранилища в :quota МБ.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Подсказка: чтобы переместить файлы и папки, перетащите их на папку или на строку навигации.",
|
||||
"Title": "Заголовок",
|
||||
"Title (optional)": "Заголовок (необязательно)",
|
||||
"TLS": "TLS",
|
||||
"to": "по",
|
||||
"To": "Кому",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Выбрано слишком много файлов. Сократите выбор и попробуйте снова.",
|
||||
"Too Many Requests": "Слишком много запросов",
|
||||
"Transactional email settings for this installation": "Настройки служебных писем этой установки",
|
||||
"Transfers — last 30 days": "Передачи — последние 30 дней",
|
||||
"Translate": "Перевод",
|
||||
"Translate It": "Перевести",
|
||||
"Turns all transactional email for this installation on or off.": "Включает или выключает все служебные письма этой установки.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Загрузил(а) файл «:subject»",
|
||||
"Uploader": "Загрузивший",
|
||||
"Uploads": "Загрузки",
|
||||
"Uploads (chart)": "Загрузки (график)",
|
||||
"URI Too Long": "URI слишком длинный",
|
||||
"URL slug": "Адресный идентификатор",
|
||||
"Use a custom link instead of a random one": "Использовать свою ссылку вместо случайной",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Он показывается только сейчас. Мы храним лишь его хеш, поэтому восстановить его позже не получится.",
|
||||
"Copy to clipboard": "Скопировать в буфер обмена",
|
||||
"Copied": "Скопировано",
|
||||
"Your tokens": "Ваши токены",
|
||||
"Revoking a token takes effect immediately.": "Отзыв токена вступает в силу немедленно.",
|
||||
"You have not created any tokens yet.": "Вы пока не создали ни одного токена.",
|
||||
"Expired": "Истёк",
|
||||
"Never used": "Ни разу не использован",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Управление дополнительными полями клиентов",
|
||||
"Moderate comments": "Модерация комментариев",
|
||||
"Only me": "Только я",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Виден каждому, кто может открыть этот файл, включая посетителей без входа в систему.",
|
||||
"Anyone, including visitors who are not logged in": "Кто угодно, включая посетителей без входа в систему",
|
||||
"Staff and clients": "Команда и клиенты",
|
||||
"Staff only": "Только команда",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Уже написанные комментарии остаются доступными для чтения, даже если файл перестал принимать новые.",
|
||||
"Comments awaiting approval": "Комментарии, ожидающие одобрения",
|
||||
"Comment settings": "Настройки комментариев",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Комментарии посетителей, не вошедших в систему. Никто их не увидит, пока вы не одобрите.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Подключите внешний S3-совместимый бакет (AWS S3, MinIO или другой S3-совместимый сервис) как хранилище для новых загрузок.",
|
||||
"Custom endpoint": "Своя конечная точка",
|
||||
"Custom field created.": "Дополнительное поле создано.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Неудавшихся задач нет.",
|
||||
"No files match these filters.": "Нет файлов, подходящих под эти фильтры.",
|
||||
"None of the selected files could be edited.": "Ни один из выбранных файлов изменить не удалось.",
|
||||
"Nothing is waiting for approval.": "Ничего не ожидает одобрения.",
|
||||
"of :count": "из :count",
|
||||
"of :quota": "из :quota",
|
||||
"on": "—",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Страница",
|
||||
"Path prefix": "Префикс пути",
|
||||
"Pending": "Ожидает",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Периодически проверяет публичный репозиторий projectsend.org на наличие более новой версии и показывает уведомление на панели. Ничего не скачивается и не применяется автоматически.",
|
||||
"Permanently failed": "Окончательно не выполнена",
|
||||
"Post comment": "Отправить комментарий",
|
||||
"Preview of :name": "Предпросмотр :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Удалить двухфакторную аутентификацию",
|
||||
"Remove two-factor authentication?": "Удалить двухфакторную аутентификацию?",
|
||||
"Require a verified email address": "Требовать подтверждённый адрес электронной почты",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Выполните их от имени пользователя, под которым работает ваш веб-сервер. Полная процедура есть в INSTALL.md.",
|
||||
"Score threshold": "Порог оценки",
|
||||
"Search timezones…": "Поиск часовых поясов…",
|
||||
"Security check failed. Please try again.": "Проверка безопасности не пройдена. Пожалуйста, попробуйте снова.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Этот сайт защищён reCAPTCHA, и применяются Политика конфиденциальности и Условия использования Google.",
|
||||
"Those files have reached their download limit.": "Эти файлы достигли своего лимита загрузок.",
|
||||
"Timezone": "Часовой пояс",
|
||||
"To update, back up your database and files, then:": "Чтобы обновить, сделайте резервную копию базы данных и файлов, затем:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Чтобы обновить, выполните шаги из INSTALL.md — сначала резервная копия, затем распакуйте выпуск и запустите миграции.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Отключайте это только если этот сервер не должен делать исходящих запросов. Если служба недоступна, пароль всё равно принимается, поэтому офлайн-установка никогда не блокируется из-за этого.",
|
||||
"Two-factor authentication removed.": "Двухфакторная аутентификация удалена.",
|
||||
"Two-factor authentication removed by an admin": "Двухфакторная аутентификация удалена администратором",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Ваша учётная запись настроена на :timezone, поэтому это не изменит того, как даты выглядят для вас.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Ваш собственный арендатор, не «common». Microsoft позволяет пользователю менять адрес электронной почты учётной записи, поэтому вход надёжен только тогда, когда токен пришёл от арендатора, указанного здесь.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Ваш поставщик отклонил секретный ключ этой установки, поэтому защищённые формы пропускаются без проверки. Проверьте секретный ключ ниже.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Ваши публичные формы защищены. Вы можете перейти на собственные ключи в любой момент."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Ваши публичные формы защищены. Вы можете перейти на собственные ключи в любой момент.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Если вы вернулись к прежней версии намеренно, выполните php artisan projectsend:update, чтобы база данных соответствовала восстановленному коду.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Он спрашивает, прежде чем проверить наличие выпуска, прежде чем скачать его и прежде чем тронуть вашу установку, и проверяет контрольную сумму того, что скачал. Полная процедура есть в UPDATE.md.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Новые файлы на месте, но база данных доведена только до :applied. Пока обновление не выполнено, эта установка работает на коде, которому её база данных не соответствует.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Периодически проверяет публичный репозиторий projectsend.org на наличие более новой версии и показывает уведомление на панели. Ничего не скачивается и не применяется автоматически — обновление всегда запускаете вы, а скрипт обновления спрашивает перед каждым шагом.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "База данных обновлена до :applied, а PHP по-прежнему отдаёт код, скомпилированный до этого. После перезагрузки это уведомление исчезнет.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "База данных обновлена до :applied, а этот контейнер по-прежнему работает на коде, с которым был запущен.",
|
||||
"This server is running version :running, but version :applied is installed": "На этом сервере работает версия :running, но установлена версия :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Чтобы обновить, выполните sudo ./update.sh в каталоге установки — скрипт спрашивает, прежде чем что-либо скачать или изменить.",
|
||||
"To update, run this in the install directory:": "Чтобы обновить, выполните это в каталоге установки:",
|
||||
"Version :running is installed, but the update has not been run": "Версия :running установлена, но обновление не выполнено",
|
||||
"Updated ProjectSend to :to, from :from": "Обновил(а) ProjectSend до :to с :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend обновлён до новой версии",
|
||||
"Come and say hello": "Заходите поздороваться",
|
||||
"Continue to the dashboard": "Перейти к панели",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Присоединиться к Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "У ProjectSend есть Discord: новости о выпусках, помощь, когда что-то ведёт себя не так, и другие люди, использующие то же ПО. Мы тоже там.",
|
||||
"What's new": "Что нового",
|
||||
"What each release brought to this installation.": "Что каждый выпуск принёс этой установке.",
|
||||
"Add the rest of your team": "Добавьте остальных участников команды",
|
||||
"Add your first client": "Добавьте первого клиента",
|
||||
"Check the scheduler is running": "Проверьте, что планировщик работает",
|
||||
"Choose how your email looks": "Выберите, как выглядят ваши письма",
|
||||
"Choose how your file lists look": "Выберите, как выглядят списки файлов",
|
||||
"Drop one in and choose who gets it.": "Перетащите файл и выберите, кто его получит.",
|
||||
"Essential": "Обязательно",
|
||||
"Expiring files and queued email need it.": "От него зависят истекающие файлы и письма в очереди.",
|
||||
"Four layouts for the client-facing pages.": "Четыре макета для страниц, которые видят клиенты.",
|
||||
"Getting started": "Первые шаги",
|
||||
"Group your clients": "Объедините клиентов в группы",
|
||||
"Made with care in Argentina. Shared with the world.": "Сделано с заботой в Аргентине. Доступно всему миру.",
|
||||
"Previewed on a real message.": "Предпросмотр на настоящем письме.",
|
||||
"Resets and share links go out through it.": "Через него уходят сбросы паролей и ссылки для доступа.",
|
||||
"Set up your mail server": "Настройте почтовый сервер",
|
||||
"Share once instead of six times.": "Поделитесь один раз вместо шести.",
|
||||
"Staff accounts, each scoped by its role.": "Учётные записи команды, каждая в рамках своей роли.",
|
||||
"Thank you for installing ProjectSend": "Спасибо, что установили ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Спасибо, что обновили ProjectSend",
|
||||
"The people you send files to.": "Люди, которым вы отправляете файлы.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Коротко о том, что стоит сделать на новой установке. Ничего из этого не устаревает.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Теперь у вас :version, и всё снова работает. Спасибо, что продолжаете доверять обмен файлами ProjectSend.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Теперь у вас :version вместо :previous, и всё снова работает. Спасибо, что продолжаете доверять обмен файлами ProjectSend.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Вы используете версию :version, и она ваша. Вот что стоит сделать в первую очередь.",
|
||||
"Thank you for choosing ProjectSend": "Спасибо, что выбрали ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Ваш сайт готов, а поддерживать его работу — наша забота. Вот что стоит сделать в первую очередь.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 сохраняет всё. Непрочитанные уведомления не удаляются никогда, каким бы старым ни было уведомление.",
|
||||
":version is available": "Доступна версия :version",
|
||||
":version on :date": ":version, :date",
|
||||
"Check now": "Проверить сейчас",
|
||||
"Checked a moment ago, so this is the answer from then.": "Проверка была только что, так что это ответ на тот момент.",
|
||||
"Checking…": "Проверка…",
|
||||
"Could not reach the update feed.": "Не удалось связаться с каналом обновлений.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Сделайте дамп базы данных, прежде чем что-либо трогать, и остановитесь, если дамп не удался.",
|
||||
"Housekeeping": "Обслуживание",
|
||||
"Keep failed jobs for": "Хранить неудавшиеся задачи",
|
||||
"Keep read notifications for": "Хранить прочитанные уведомления",
|
||||
"Last checked :date": "Последняя проверка: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Не трогать PHP и worker — на случай, если вы перезапускаете их сами.",
|
||||
"Other options": "Другие параметры",
|
||||
"Report which version is installed and which is available, and change nothing.": "Сообщает, какая версия установлена и какая доступна, и ничего не меняет.",
|
||||
"Retention updated.": "Срок хранения обновлён.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Запущенный без параметров скрипт спрашивает о каждом из этих пунктов по ходу дела. Эти — для запусков, где ответ вам уже известен.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Пропускает вопрос о резервной копии — для запуска, за которым вы не следите.",
|
||||
"The latest release (:tag) is not a version this can compare.": "Последний выпуск (:tag) — не та версия, которую здесь можно сравнить.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Две вещи накапливаются сами по себе. Это сроки, которых придерживаются ежедневные очистки выше.",
|
||||
"Up to date": "Актуальная версия",
|
||||
"Update checks are not available on this edition.": "Проверка обновлений недоступна в этой редакции.",
|
||||
"Update options": "Параметры обновления",
|
||||
"Updated": "Обновлено",
|
||||
"Version :version is available. You are running :current.": "Доступна версия :version. У вас :current.",
|
||||
"You are up to date, running version :current.": "У вас актуальная версия :current.",
|
||||
"days": "дней",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help выводит все параметры, а UPDATE.md описывает всю процедуру, включая ручную.",
|
||||
"Fetch dashboard news": "Получить новости для панели",
|
||||
"Purge API request logs": "Очистить журнал запросов API",
|
||||
"Purge erased accounts": "Очистить стёртые учётные записи",
|
||||
"Purge expired files": "Очистить файлы с истёкшим сроком",
|
||||
"Purge failed jobs": "Очистить неудавшиеся задачи",
|
||||
"Purge orphan files": "Очистить осиротевшие файлы",
|
||||
"Purge read notifications": "Очистить прочитанные уведомления",
|
||||
"Purge stale chunked uploads": "Очистить брошенные частичные загрузки",
|
||||
"Purge zip downloads": "Очистить ZIP-архивы для скачивания",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Соответствует этому фильтру: :count, начиная с последнего",
|
||||
"A file was previewed via the public group listing": "Файл просмотрен через публичный список групп",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Любой, у кого есть ссылка, может открыть изображение, видео, аудиофайл или PDF в браузере, не скачивая его. Отключите, если единственным способом увидеть публичный файл должно быть скачивание.",
|
||||
"Clients can preview files": "Клиенты могут просматривать файлы",
|
||||
"Downloads & previews": "Скачивания и просмотры",
|
||||
"Downloads: :count": "Скачиваний: :count",
|
||||
"Let visitors preview public files": "Разрешить посетителям просматривать публичные файлы",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Позволяет клиентам открывать изображение, видео, аудиофайл или PDF прямо в портале, а не только скачивать. Отключите, если единственным способом увидеть файл должно быть скачивание. Команда может просматривать файлы всегда.",
|
||||
"No downloads match these filters.": "Нет скачиваний, подходящих под эти фильтры.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Этот файл пока никто не скачивал и не просматривал.",
|
||||
"Preview :name": "Просмотреть :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Просмотрел(а) файл «:subject» через публичный список групп",
|
||||
"Previews: :count": "Просмотров: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Эта установка собирает образы сама, поэтому загрузка готового образа ничего не изменит. Если в выпуске изменились composer.lock или фронтенд, выполните также composer install и npm run build — полная процедура описана в UPDATE.md.",
|
||||
"To update, run this in the directory you cloned:": "Чтобы обновить, выполните это в каталоге, который вы клонировали:",
|
||||
"View all previews (:count)": "Показать все просмотры (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Ваш браузер не может воспроизвести этот файл. Скачайте его, чтобы посмотреть."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": "Vipakuliwa :count",
|
||||
":count file deleted.|:count files deleted.": "Faili :count limefutwa.|Mafaili :count yamefutwa.",
|
||||
":count file imported.|:count files imported.": "Faili :count limeingizwa.|Mafaili :count yameingizwa.",
|
||||
":count files": "Mafaili :count",
|
||||
":count folders": "Folda :count",
|
||||
":count items have been shared with you": "Vipengele :count umeshirikishwa",
|
||||
":count members": "Wanachama :count",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(na :count makosa zaidi)",
|
||||
"(and :count more errors)": "(na :count makosa zaidi)|(na :count makosa zaidi)|(na :count makosa zaidi)",
|
||||
"(deleted account)": "(akaunti iliyofutwa)",
|
||||
"0 means unlimited.": "0 humaanisha bila kikomo.",
|
||||
"A client account request was approved": "Ombi la akaunti ya mteja limeidhinishwa",
|
||||
"A client account request was denied": "Ombi la akaunti ya mteja limekataliwa",
|
||||
"A client left a group": "Mteja ameondoka kwenye kikundi",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Akaunti mpya ya mteja imeundwa: :name (:email).",
|
||||
"A new client has registered": "Mteja mpya amejisajili",
|
||||
"A new ProjectSend version is available": "Toleo jipya la ProjectSend linapatikana",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Ukishaweka kikundi kuwa cha umma, ukurasa wake hufikika daima kupitia kiungo chake — sehemu hii hudhibiti tu anwani ya msingi na orodha ya hiari inayoonyesha vikundi na mafaili yote ya umma kwa pamoja.",
|
||||
"A public link was created": "Kiungo cha umma kimeundwa",
|
||||
"A public link was revoked": "Kiungo cha umma kimetenguliwa",
|
||||
"A role was created": "Jukumu limeundwa",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Usimamizi: kumeombwa uanachama wa kikundi",
|
||||
"Admin: new client registered": "Usimamizi: mteja mpya amejisajili",
|
||||
"Administration": "Usimamizi",
|
||||
"After uploading you can assign it to clients and groups": "Baada ya kupakia utaweza kuligawia wateja na vikundi",
|
||||
"Agree": "Kubali",
|
||||
"All": "Zote",
|
||||
"All actions": "Vitendo vyote",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Maoni yote",
|
||||
"All failed jobs deleted.": "Kazi zote zilizoshindwa zimefutwa.",
|
||||
"All files": "Mafaili yote",
|
||||
"All groups": "Vikundi vyote",
|
||||
"All permissions": "Ruhusa zote",
|
||||
"All rights reserved.": "Haki zote zimehifadhiwa.",
|
||||
"All roles": "Majukumu yote",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Yeyote anayekitumia hataweza tena kupakua faili hilo.",
|
||||
"Appearance": "Muonekano",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Hutumika kwenye kurasa za umma za vikundi na mafaili kwa wageni, na kwenye lango la mteja \"Mafaili yangu\".",
|
||||
"Apply filters": "Tumia vichujio",
|
||||
"Approve": "Idhinisha",
|
||||
"Approve account requests": "Kuidhinisha maombi ya akaunti",
|
||||
"Approve group membership requests": "Kuidhinisha maombi ya uanachama wa vikundi",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Una uhakika unataka kufuta akaunti yako?",
|
||||
"Assign": "Kadiria",
|
||||
"Assign categories to files": "Kugawia mafaili kategoria",
|
||||
"Assigned": "Imegawiwa",
|
||||
"Assigned clients": "Wateja waliogawiwa",
|
||||
"Assigned the file \":subject\" to :target": "Amegawia faili \":subject\" kwa :target",
|
||||
"Associate": "Mshirika",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Mwandishi",
|
||||
"Available groups": "Vikundi vinavyopatikana",
|
||||
"Available placeholders": "Vishika-nafasi vinavyopatikana",
|
||||
"Back to My files": "Rudi kwenye Mafaili yangu",
|
||||
"Bad Gateway": "Mlango Mbaya",
|
||||
"Bad Request": "Ombi baya",
|
||||
"Bandwidth Limit Exceeded": "Kikomo cha Kipimo Kimezidi",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Maandishi ya ujumbe",
|
||||
"Browse": "Vinjari",
|
||||
"Browse publicly shared groups and files below.": "Vinjari hapa chini vikundi na mafaili yaliyoshirikishwa hadharani.",
|
||||
"Built-in": "Ya asili",
|
||||
"Built-in role names cannot be changed.": "Majina ya majukumu ya asili hayawezi kubadilishwa.",
|
||||
"Built-in roles cannot be deleted.": "Majukumu ya asili hayawezi kufutwa.",
|
||||
"Call to action": "Kitufe cha hatua",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Thibitisha nenosiri",
|
||||
"Conflict": "Migogoro",
|
||||
"Connect": "Unganisha",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Unganisha bucket ya nje — inayooana na S3 (AWS S3, MinIO, Backblaze) au Google Cloud Storage — kama hifadhi ya mafaili mapya yanayopakiwa.",
|
||||
"Connection Closed Without Response": "Muunganisho Umefungwa Bila Majibu",
|
||||
"Connection Timed Out": "Muda wa Muunganisho Umekwisha",
|
||||
"Continue": "Endelea",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Zima akaunti yako sasa; itafutwa kabisa baada ya siku :days",
|
||||
"Deactivated the account \":subject\"": "Ameizima akaunti \":subject\"",
|
||||
"Default": "Chaguomsingi",
|
||||
"Defaults to the file name.": "Kwa chaguomsingi hutumia jina la faili.",
|
||||
"Delete": "Futa",
|
||||
"Delete :name": "Futa :name",
|
||||
"Delete account": "Futa akaunti",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Kataa",
|
||||
"Deny this request?": "Kataa ombi hili?",
|
||||
"Description": "Maelezo",
|
||||
"Description (optional)": "Maelezo (si lazima)",
|
||||
"Detach": "Tenganisha",
|
||||
"Details": "Maelezo",
|
||||
"Disable": "Zima",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Kwenda kwa ukurasa :page",
|
||||
"Go to the login form": "Nenda kwenye fomu ya kuingia",
|
||||
"Gone": "Imeondoka",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Kijivu",
|
||||
"Green": "Kijani",
|
||||
"Group": "Kikundi",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Ingia",
|
||||
"Logout": "Toka nje",
|
||||
"Loop Detected": "Kitanzi Kimegunduliwa",
|
||||
"Mail provider settings saved.": "Mipangilio ya mtoa huduma wa barua pepe imehifadhiwa.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Hali ya Matengenezo",
|
||||
"Manage custom fields": "Kusimamia sehemu maalum",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Bado hujashirikishwa faili lolote.",
|
||||
"No files here yet.": "Bado hakuna mafaili hapa.",
|
||||
"No files or folders match your search.": "Hakuna faili wala folda inayolingana na utafutaji wako.",
|
||||
"No files yet. Upload the first one.": "Bado hakuna mafaili. Pakia la kwanza.",
|
||||
"No folder": "Hakuna folda",
|
||||
"No groups match these filters.": "Hakuna kikundi kinacholingana na vichujio hivi.",
|
||||
"No groups yet.": "Bado hakuna vikundi.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Bado hakuna mpokeaji aliyeongezwa.",
|
||||
"No requests match your search.": "Hakuna ombi linalolingana na utafutaji wako.",
|
||||
"No roles match this filter.": "Hakuna jukumu linalolingana na kichujio hiki.",
|
||||
"No service account key has been saved yet.": "Bado hakuna ufunguo wa akaunti ya huduma uliohifadhiwa.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Hakuna kikomo cha ukubwa. Upakiaji unaweza kusimamishwa na huendelea wenyewe baada ya kukatika.",
|
||||
"No users match these filters.": "Hakuna mtumiaji anayelingana na vichujio hivi.",
|
||||
"No users yet.": "Bado hakuna watumiaji.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Chungwa",
|
||||
"Origin Is Unreachable": "Asili Haifikiki",
|
||||
"Outgoing email": "Barua pepe zinazotoka",
|
||||
"Outgoing email theme": "Mandhari ya barua pepe zinazotoka",
|
||||
"Page :current of :last": "Ukurasa :current kati ya :last",
|
||||
"Page Expired": "Ukurasa umeisha Muda Wake",
|
||||
"Pagination Navigation": "Urambazaji Kurasa",
|
||||
"Partial Content": "Maudhui Sehemu",
|
||||
"Password": "Nenosiri",
|
||||
"Password reset": "Kuweka upya nenosiri",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Bandika faili la ufunguo la JSON la akaunti ya huduma yenye ruhusa ya kusoma na kuandika vitu ndani ya bucket. Huhifadhiwa kwa usimbaji fiche na hauonyeshwi tena.",
|
||||
"Path": "Njia",
|
||||
"Payload Too Large": "Mzigo Mkubwa Sana",
|
||||
"Payment Required": "Malipo yanahitajika",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Mipangilio ya orodha ya umma",
|
||||
"Public listing URL": "Anwani ya orodha ya umma",
|
||||
"Public pages & portal": "Kurasa za umma na lango",
|
||||
"Public pages and client portal theme": "Mandhari ya kurasa za umma na lango la mteja",
|
||||
"Published": "Ilichapishwa",
|
||||
"Purple": "Zambarau",
|
||||
"Quota": "Kiasi kinachoruhusiwa",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Kurejesha",
|
||||
"Restore :name": "Rejesha :name",
|
||||
"Restricted extension": "Kiendelezi kilichozuiwa",
|
||||
"Result": "Matokeo",
|
||||
"results": "matokeo",
|
||||
"Review membership requests": "Kupitia maombi ya uanachama",
|
||||
"Review pending requests": "Pitia maombi yanayosubiri",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Jina la jukumu",
|
||||
"Role updated.": "Jukumu limesasishwa.",
|
||||
"Roles": "Majukumu",
|
||||
"S3-compatible": "Inayooana na S3",
|
||||
"Sample email": "Barua pepe ya mfano",
|
||||
"Save": "Hifadhi",
|
||||
"Save :name": "Hifadhi :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Hutuma barua pepe halisi sasa hivi bila kupitia foleni, ili uthibitishe kuwa mipangilio ya juu inafanya kazi kweli.",
|
||||
"Separate paragraphs with a blank line.": "Tenganisha aya kwa mstari mtupu.",
|
||||
"Server Error": "Hitilafu ya Seva",
|
||||
"Service account key": "Ufunguo wa akaunti ya huduma",
|
||||
"Service Unavailable": "Huduma Hazipatikani",
|
||||
"Session Has Expired": "Kipindi Kimeisha",
|
||||
"Set file expiration dates": "Kuweka tarehe za mwisho za mafaili",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Mipangilio",
|
||||
"Settings that apply to the whole installation": "Mipangilio inayohusu usakinishaji mzima",
|
||||
"Setup complete": "Usanidi umekamilika",
|
||||
"Share a file with the people who manage your account.": "Shirikisha faili na watu wanaosimamia akaunti yako.",
|
||||
"Share files with several clients at once": "Shirikisha mafaili na wateja kadhaa kwa mpigo",
|
||||
"Shared the folder \":subject\" with :target": "Ameshirikisha folda \":subject\" na :target",
|
||||
"Shared with": "Imeshirikishwa na",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Kubadilisha Itifaki",
|
||||
"System": "Mfumo",
|
||||
"System Administrator": "Msimamizi wa mfumo",
|
||||
"System settings": "Mipangilio ya mfumo",
|
||||
"System settings were updated": "Mipangilio ya mfumo imesasishwa",
|
||||
"System users": "Watumiaji wa mfumo",
|
||||
"Tag": "Lebo",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Jaribio",
|
||||
"Text": "Maandishi",
|
||||
"Text or author": "Maandishi au mwandishi",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Hii haionekani kuwa faili la ufunguo wa akaunti ya huduma: si JSON halali.",
|
||||
"That service account key file is missing its :field.": "Faili hili la ufunguo wa akaunti ya huduma linakosa :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Akaunti ya :name itafutwa kabisa. Hatua hii haiwezi kutenduliwa.",
|
||||
"The account password was changed": "Nenosiri la akaunti limebadilishwa",
|
||||
"The account request of :name will be denied and the account deleted.": "Ombi la akaunti la :name litakataliwa na akaunti ifutwe.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Vikundi ulivyo mwanachama na vile unavyoweza kuomba kujiunga",
|
||||
"The header/footer style used for every outgoing notification email.": "Mtindo wa kichwa na mkia unaotumika kwa kila barua pepe ya arifa inayotoka.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Muonekano wa kurasa za umma kwa wageni, lango la mteja, na barua pepe za arifa zinazotoka.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Seva ya barua pepe yenyewe husanidiwa kwenye faili la .env la usakinishaji huu (vigezo vya MAIL_*), si hapa.",
|
||||
"The people who administer this installation and upload files": "Watu wanaosimamia usakinishaji huu na kupakia mafaili",
|
||||
"The people you share files with": "Watu unaowashirikisha mafaili",
|
||||
"The provided two-factor authentication code was invalid.": "Msimbo wa uthibitishaji wa hatua mbili uliowekwa si sahihi.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Bado hakuna cha kusanidi.",
|
||||
"Test email from :site": "Barua pepe ya majaribio kutoka :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Mafaili haya yataondolewa kabisa kwenye diski. Hatua hii haiwezi kutenduliwa.",
|
||||
"Theme": "Mandhari",
|
||||
"Theme updated.": "Mandhari yamesasishwa.",
|
||||
"Theming": "Mandhari",
|
||||
"Theming settings": "Mipangilio ya mandhari",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Upakiaji huu ungezidi kiasi chako cha hifadhi cha MB :quota.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Dokezo: buruta mafaili na folda hadi kwenye folda au njia ya urambazaji ili kuvihamisha.",
|
||||
"Title": "Kichwa",
|
||||
"Title (optional)": "Kichwa (si lazima)",
|
||||
"TLS": "TLS",
|
||||
"to": "hadi",
|
||||
"To": "Kwa",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Umechagua mafaili mengi mno. Punguza uteuzi kisha jaribu tena.",
|
||||
"Too Many Requests": "Maombi Mengi Mno",
|
||||
"Transactional email settings for this installation": "Mipangilio ya barua pepe za kiutendaji kwa usakinishaji huu",
|
||||
"Transfers — last 30 days": "Uhamishaji — siku 30 zilizopita",
|
||||
"Translate": "Tafsiri",
|
||||
"Translate It": "Itafsiri",
|
||||
"Turns all transactional email for this installation on or off.": "Huwasha au huzima barua pepe zote za kiutendaji za usakinishaji huu.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Amepakia faili \":subject\"",
|
||||
"Uploader": "Mpakiaji",
|
||||
"Uploads": "Vilivyopakiwa",
|
||||
"Uploads (chart)": "Vilivyopakiwa (chati)",
|
||||
"URI Too Long": "URI Mrefu Sana",
|
||||
"URL slug": "Kitambulisho cha anwani",
|
||||
"Use a custom link instead of a random one": "Tumia kiungo cha kwako badala ya cha nasibu",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Hii ndiyo mara pekee itakapoonyeshwa. Tunahifadhi alama yake ya siri tu, hivyo haiwezi kurejeshwa baadaye.",
|
||||
"Copy to clipboard": "Nakili kwenye ubao wa kunakili",
|
||||
"Copied": "Imenakiliwa",
|
||||
"Your tokens": "Tokeni zako",
|
||||
"Revoking a token takes effect immediately.": "Kutengua tokeni huanza kutumika mara moja.",
|
||||
"You have not created any tokens yet.": "Bado hujaunda tokeni yoyote.",
|
||||
"Expired": "Imeisha muda",
|
||||
"Never used": "Haijawahi kutumika",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Kusimamia sehemu maalum za wateja",
|
||||
"Moderate comments": "Kusimamia maoni",
|
||||
"Only me": "Mimi pekee",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Huonekana kwa yeyote anayeweza kufungua faili hili, wakiwemo wageni ambao hawajaingia.",
|
||||
"Anyone, including visitors who are not logged in": "Yeyote, wakiwemo wageni ambao hawajaingia",
|
||||
"Staff and clients": "Wafanyakazi na wateja",
|
||||
"Staff only": "Wafanyakazi pekee",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Maoni yaliyoandikwa tayari hubaki yanasomeka hata kama faili litaacha kupokea mapya.",
|
||||
"Comments awaiting approval": "Maoni yanayosubiri idhini",
|
||||
"Comment settings": "Mipangilio ya maoni",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Maoni yaliyoachwa na wageni ambao hawajaingia. Hakuna anayeyaona hadi uyaidhinishe.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Unganisha bucket ya nje inayooana na S3 (AWS S3, MinIO, au huduma nyingine inayooana na S3) kama hifadhi ya mafaili mapya yanayopakiwa.",
|
||||
"Custom endpoint": "Sehemu maalum ya API",
|
||||
"Custom field created.": "Sehemu maalum imeundwa.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Hakuna kazi zilizoshindwa.",
|
||||
"No files match these filters.": "Hakuna faili linalolingana na vichujio hivi.",
|
||||
"None of the selected files could be edited.": "Hakuna faili lolote lililochaguliwa lililoweza kuhaririwa.",
|
||||
"Nothing is waiting for approval.": "Hakuna kinachosubiri idhini.",
|
||||
"of :count": "kati ya :count",
|
||||
"of :quota": "kati ya :quota",
|
||||
"on": "tarehe",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Ukurasa",
|
||||
"Path prefix": "Kianzio cha njia",
|
||||
"Pending": "Inasubiri",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Hukagua mara kwa mara hazina ya umma ya projectsend.org kuona kama kuna toleo jipya na huonyesha taarifa kwenye dashibodi. Hakuna kinachopakuliwa au kusakinishwa kiotomatiki.",
|
||||
"Permanently failed": "Imeshindwa kabisa",
|
||||
"Post comment": "Tuma maoni",
|
||||
"Preview of :name": "Hakikisho la :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Ondoa uthibitishaji wa hatua mbili",
|
||||
"Remove two-factor authentication?": "Ondoa uthibitishaji wa hatua mbili?",
|
||||
"Require a verified email address": "Hitaji anwani ya barua pepe iliyothibitishwa",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Endesha hizi kama mtumiaji ambaye seva yako ya wavuti inaendeshwa kwake. INSTALL.md ina utaratibu kamili.",
|
||||
"Score threshold": "Kizingiti cha alama",
|
||||
"Search timezones…": "Tafuta saa za maeneo…",
|
||||
"Security check failed. Please try again.": "Ukaguzi wa usalama umeshindwa. Tafadhali jaribu tena.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Tovuti hii inalindwa na reCAPTCHA na Sera ya Faragha na Masharti ya Huduma ya Google yanatumika.",
|
||||
"Those files have reached their download limit.": "Faili hizo zimefikia kikomo chake cha upakuaji.",
|
||||
"Timezone": "Saa za eneo",
|
||||
"To update, back up your database and files, then:": "Ili kusasisha, hifadhi nakala ya hifadhidata na faili zako, kisha:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Ili kusasisha, fuata hatua katika INSTALL.md — hifadhi nakala kwanza, kisha fungua toleo na uendeshe uhamiaji.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Zima hii tu iwapo seva hii haipaswi kufanya maombi yoyote ya kutoka nje. Iwapo huduma haiwezi kufikiwa nywila inakubaliwa hata hivyo, kwa hivyo usakinishaji wa nje ya mtandao hauzuiwi kamwe nayo.",
|
||||
"Two-factor authentication removed.": "Uthibitishaji wa hatua mbili umeondolewa.",
|
||||
"Two-factor authentication removed by an admin": "Uthibitishaji wa hatua mbili uliondolewa na msimamizi",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Akaunti yako mwenyewe imewekwa kwa :timezone, kwa hivyo hii haitabadilisha jinsi tarehe zinavyoonekana kwako.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Mpangaji wako mwenyewe, si \"common\". Microsoft huruhusu mtumiaji kubadilisha anwani ya barua pepe kwenye akaunti yake, kwa hivyo kuingia kunaaminika tu wakati tokeni ilitoka kwa mpangaji uliyemtaja hapa.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Mtoa huduma wako alikataa ufunguo wa siri wa usakinishaji huu, kwa hivyo fomu zilizolindwa zinapitishwa bila kukaguliwa. Angalia ufunguo wa siri hapa chini.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Fomu zako za umma zinalindwa. Unaweza kubadilisha na kutumia funguo zako mwenyewe wakati wowote."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Fomu zako za umma zinalindwa. Unaweza kubadilisha na kutumia funguo zako mwenyewe wakati wowote.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Ikiwa ulirudi kwenye toleo la awali kwa makusudi, endesha php artisan projectsend:update ili hifadhidata ilingane na msimbo uliourejesha.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Huuliza kabla ya kutafuta toleo, kabla ya kulipakua, na kabla ya kugusa usakinishaji wako — na huthibitisha checksum ya kile inachopakua. UPDATE.md ina utaratibu kamili.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Faili mpya zipo tayari, lakini hifadhidata imefikishwa hadi :applied pekee. Hadi usasishaji utakapoendeshwa, usakinishaji huu unatumia msimbo usiolingana na hifadhidata yake.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Hukagua mara kwa mara hazina ya umma ya projectsend.org kuona kama kuna toleo jipya na huonyesha taarifa kwenye dashibodi. Hakuna kinachopakuliwa au kusakinishwa kiotomatiki — kusasisha ni jambo unalofanya wewe daima, na hati ya kusasisha huuliza kabla ya kila hatua.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Hifadhidata ilisasishwa hadi :applied, na PHP bado inatoa msimbo iliokusanya kabla ya hapo. Ukiipakia upya, taarifa hii itaondoka.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Hifadhidata ilisasishwa hadi :applied, na kontena hili bado linaendesha msimbo lililoanza nao.",
|
||||
"This server is running version :running, but version :applied is installed": "Seva hii inaendesha toleo :running, lakini toleo lililosakinishwa ni :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Ili kusasisha, endesha sudo ./update.sh katika saraka ya usakinishaji — huuliza kabla ya kupakua au kubadilisha chochote.",
|
||||
"To update, run this in the install directory:": "Ili kusasisha, endesha hii katika saraka ya usakinishaji:",
|
||||
"Version :running is installed, but the update has not been run": "Toleo :running limesakinishwa, lakini usasishaji haujaendeshwa",
|
||||
"Updated ProjectSend to :to, from :from": "Amesasisha ProjectSend hadi :to, kutoka :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend imesasishwa hadi toleo jipya",
|
||||
"Come and say hello": "Karibu useme habari",
|
||||
"Continue to the dashboard": "Endelea kwenye dashibodi",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Jiunge na Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend ina Discord: habari za matoleo, msaada pale kitu kinapokwenda kombo, na watu wengine wanaotumia programu hiyo hiyo. Sisi pia tupo humo.",
|
||||
"What's new": "Mapya",
|
||||
"What each release brought to this installation.": "Kile kila toleo kimeleta kwenye usakinishaji huu.",
|
||||
"Add the rest of your team": "Ongeza wengine wa timu yako",
|
||||
"Add your first client": "Ongeza mteja wako wa kwanza",
|
||||
"Check the scheduler is running": "Hakikisha kipanga ratiba kinafanya kazi",
|
||||
"Choose how your email looks": "Chagua muonekano wa barua pepe zako",
|
||||
"Choose how your file lists look": "Chagua muonekano wa orodha zako za mafaili",
|
||||
"Drop one in and choose who gets it.": "Weka faili moja na uchague nani anapata.",
|
||||
"Essential": "Muhimu sana",
|
||||
"Expiring files and queued email need it.": "Mafaili yanayoisha muda na barua pepe zilizopangwa zinakitegemea.",
|
||||
"Four layouts for the client-facing pages.": "Miundo minne ya kurasa wanazoona wateja.",
|
||||
"Getting started": "Kuanza",
|
||||
"Group your clients": "Panga wateja wako katika vikundi",
|
||||
"Made with care in Argentina. Shared with the world.": "Imetengenezwa kwa uangalifu Argentina. Imeshirikiwa na dunia.",
|
||||
"Previewed on a real message.": "Inaonyeshwa kwenye ujumbe halisi.",
|
||||
"Resets and share links go out through it.": "Kuweka upya nywila na viungo vya kushiriki hupita hapo.",
|
||||
"Set up your mail server": "Sanidi seva yako ya barua pepe",
|
||||
"Share once instead of six times.": "Shiriki mara moja badala ya mara sita.",
|
||||
"Staff accounts, each scoped by its role.": "Akaunti za wafanyakazi, kila moja ikiwa na mipaka ya jukumu lake.",
|
||||
"Thank you for installing ProjectSend": "Asante kwa kusakinisha ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Asante kwa kusasisha ProjectSend",
|
||||
"The people you send files to.": "Watu unaowatumia mafaili.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Muhtasari wa cha kufanya kwenye usakinishaji mpya. Hakuna kinachoisha muda hapa.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Sasa uko kwenye :version na kila kitu kimerudi kufanya kazi. Asante kwa kuendelea kuamini ProjectSend kwa kushiriki mafaili yako.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Sasa uko kwenye :version, kutoka :previous, na kila kitu kimerudi kufanya kazi. Asante kwa kuendelea kuamini ProjectSend kwa kushiriki mafaili yako.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Unatumia toleo :version, na ni lako. Haya ndiyo yanayofaa kufanywa kwanza.",
|
||||
"Thank you for choosing ProjectSend": "Asante kwa kuchagua ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Tovuti yako iko tayari, na kuiendesha ni kazi yetu. Haya ndiyo yanayofaa kufanywa kwanza.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 huhifadhi kila kitu. Arifa ambazo hazijasomwa hazifutwi kamwe, hata ziwe za zamani kiasi gani.",
|
||||
":version is available": ":version inapatikana",
|
||||
":version on :date": ":version tarehe :date",
|
||||
"Check now": "Angalia sasa",
|
||||
"Checked a moment ago, so this is the answer from then.": "Ukaguzi ulifanyika punde tu, hivyo hili ni jibu la wakati huo.",
|
||||
"Checking…": "Inaangalia…",
|
||||
"Could not reach the update feed.": "Imeshindwa kufikia mkondo wa masasisho.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Hifadhi nakala ya hifadhidata kabla ya kugusa chochote, na simama ikiwa uhifadhi utashindwa.",
|
||||
"Housekeeping": "Usafi wa kawaida",
|
||||
"Keep failed jobs for": "Hifadhi kazi zilizoshindwa kwa",
|
||||
"Keep read notifications for": "Hifadhi arifa zilizosomwa kwa",
|
||||
"Last checked :date": "Ilikaguliwa mwisho: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Usiguse PHP wala worker, kwa pale unapoziwasha upya mwenyewe.",
|
||||
"Other options": "Chaguo nyingine",
|
||||
"Report which version is installed and which is available, and change nothing.": "Huripoti toleo lililosakinishwa na lile linalopatikana, bila kubadilisha chochote.",
|
||||
"Retention updated.": "Muda wa kuhifadhi umesasishwa.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Ukiendesha hati bila chaguo, itauliza kila mojawapo ya haya njiani. Haya ni kwa uendeshaji ambapo tayari unajua jibu.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Huruka swali kuhusu nakala rudufu, kwa uendeshaji ambao huketi mbele yake.",
|
||||
"The latest release (:tag) is not a version this can compare.": "Toleo jipya zaidi (:tag) si toleo linaloweza kulinganishwa hapa.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Vitu viwili hujilundika vyenyewe. Hii ndiyo mipaka ya muda ambayo usafi wa kila siku hapo juu hufuata.",
|
||||
"Up to date": "Ni la kisasa",
|
||||
"Update checks are not available on this edition.": "Ukaguzi wa masasisho haupatikani katika toleo hili.",
|
||||
"Update options": "Chaguo za kusasisha",
|
||||
"Updated": "Ilisasishwa",
|
||||
"Version :version is available. You are running :current.": "Toleo :version linapatikana. Unatumia :current.",
|
||||
"You are up to date, running version :current.": "Uko na toleo jipya zaidi :current.",
|
||||
"days": "siku",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help huorodhesha kila chaguo, na UPDATE.md hupitia utaratibu mzima, ikiwemo kuufanya kwa mkono.",
|
||||
"Fetch dashboard news": "Leta habari za dashibodi",
|
||||
"Purge API request logs": "Safisha kumbukumbu za maombi ya API",
|
||||
"Purge erased accounts": "Safisha akaunti zilizofutwa kabisa",
|
||||
"Purge expired files": "Safisha mafaili yaliyoisha muda",
|
||||
"Purge failed jobs": "Safisha kazi zilizoshindwa",
|
||||
"Purge orphan files": "Safisha mafaili yatima",
|
||||
"Purge read notifications": "Safisha arifa zilizosomwa",
|
||||
"Purge stale chunked uploads": "Safisha upakiaji wa vipande ulioachwa",
|
||||
"Purge zip downloads": "Safisha vipakuliwa vya ZIP",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Zinazolingana na kichujio hiki: :count, mpya kwanza",
|
||||
"A file was previewed via the public group listing": "Faili limehakikiwa kupitia orodha ya umma ya vikundi",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Yeyote mwenye kiungo anaweza kufungua picha, video, faili la sauti au PDF katika kivinjari badala ya kulipakua. Zima ili kupakua kuwe njia pekee ya kuona faili la umma.",
|
||||
"Clients can preview files": "Wateja wanaweza kuhakiki faili",
|
||||
"Downloads & previews": "Vipakuliwa na hakiki",
|
||||
"Downloads: :count": "Vipakuliwa: :count",
|
||||
"Let visitors preview public files": "Ruhusu wageni kuhakiki faili za umma",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Huruhusu wateja kufungua picha, video, faili la sauti au PDF ndani ya lango badala ya kulipakua tu. Zima ili kupakua kuwe njia pekee ya kuona faili. Wafanyakazi wanaweza kuhakiki kila wakati.",
|
||||
"No downloads match these filters.": "Hakuna kipakuliwa kinacholingana na vichujio hivi.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Bado hakuna aliyepakua wala kuhakiki faili hili.",
|
||||
"Preview :name": "Hakiki :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Amehakiki faili \":subject\" kupitia orodha ya umma ya vikundi",
|
||||
"Previews: :count": "Hakiki: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Usakinishaji huu hujenga picha zake mwenyewe, kwa hivyo kuvuta picha hakubadilishi chochote. Ikiwa toleo limebadilisha composer.lock au sehemu ya mbele, endesha pia composer install na npm run build — UPDATE.md ina utaratibu kamili.",
|
||||
"To update, run this in the directory you cloned:": "Ili kusasisha, endesha hii katika saraka uliyoiklona:",
|
||||
"View all previews (:count)": "Angalia hakiki zote (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Kivinjari chako hakiwezi kucheza faili hili. Lipakue ili kuliona."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count indirme",
|
||||
":count file deleted.|:count files deleted.": ":count dosya silindi.|:count dosya silindi.",
|
||||
":count file imported.|:count files imported.": ":count dosya içe aktarıldı.|:count dosya içe aktarıldı.",
|
||||
":count files": ":count dosya",
|
||||
":count folders": ":count klasör",
|
||||
":count items have been shared with you": "Sizinle :count öğe paylaşıldı",
|
||||
":count members": ":count üye",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(ve :count hata daha var)",
|
||||
"(and :count more errors)": "(ve :count hata daha var)|(ve :count hata daha var)|(ve :count hata daha var)",
|
||||
"(deleted account)": "(silinmiş hesap)",
|
||||
"0 means unlimited.": "0, sınırsız anlamına gelir.",
|
||||
"A client account request was approved": "Bir müşteri hesabı başvurusu onaylandı",
|
||||
"A client account request was denied": "Bir müşteri hesabı başvurusu reddedildi",
|
||||
"A client left a group": "Bir müşteri gruptan ayrıldı",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Yeni bir müşteri hesabı oluşturuldu: :name (:email).",
|
||||
"A new client has registered": "Yeni bir müşteri kaydoldu",
|
||||
"A new ProjectSend version is available": "Yeni bir ProjectSend sürümü mevcut",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Herkese açık olarak işaretlediğiniz bir grubun sayfasına kendi bağlantısıyla her zaman ulaşılabilir — bu bölüm yalnızca temel adresi ve tüm herkese açık grupları ve dosyaları bir arada listeleyen isteğe bağlı dizini denetler.",
|
||||
"A public link was created": "Herkese açık bir bağlantı oluşturuldu",
|
||||
"A public link was revoked": "Herkese açık bir bağlantı iptal edildi",
|
||||
"A role was created": "Bir rol oluşturuldu",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Yönetim: grup üyeliği talep edildi",
|
||||
"Admin: new client registered": "Yönetim: yeni müşteri kaydoldu",
|
||||
"Administration": "Yönetim",
|
||||
"After uploading you can assign it to clients and groups": "Yükledikten sonra müşterilere ve gruplara atayabilirsiniz",
|
||||
"Agree": "Kabul etmek",
|
||||
"All": "Tümü",
|
||||
"All actions": "Tüm işlemler",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Tüm yorumlar",
|
||||
"All failed jobs deleted.": "Başarısız işlerin tümü silindi.",
|
||||
"All files": "Tüm dosyalar",
|
||||
"All groups": "Tüm gruplar",
|
||||
"All permissions": "Tüm izinler",
|
||||
"All rights reserved.": "Tüm hakları saklıdır.",
|
||||
"All roles": "Tüm roller",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Onu kullanan hiç kimse dosyayı artık indiremeyecek.",
|
||||
"Appearance": "Görünüm",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Ziyaretçilere açık grup ve dosya sayfaları ile \"Dosyalarım\" müşteri portalı için geçerlidir.",
|
||||
"Apply filters": "Filtreleri uygula",
|
||||
"Approve": "Onayla",
|
||||
"Approve account requests": "Hesap başvurularını onaylama",
|
||||
"Approve group membership requests": "Grup üyeliği başvurularını onaylama",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Hesabınızı silmek istediğinizden emin misiniz?",
|
||||
"Assign": "Atamak",
|
||||
"Assign categories to files": "Dosyalara kategori atama",
|
||||
"Assigned": "Atandı",
|
||||
"Assigned clients": "Atanmış müşteriler",
|
||||
"Assigned the file \":subject\" to :target": "\":subject\" dosyasını :target hedefine atadı",
|
||||
"Associate": "İş arkadaşı",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Yazar",
|
||||
"Available groups": "Kullanılabilir gruplar",
|
||||
"Available placeholders": "Kullanılabilir yer tutucular",
|
||||
"Back to My files": "Dosyalarım'a dön",
|
||||
"Bad Gateway": "Geçersiz Ağ Geçidi",
|
||||
"Bad Request": "Geçersiz istek",
|
||||
"Bandwidth Limit Exceeded": "Bant Genişliği Sınırı Aşıldı",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Mesaj metni",
|
||||
"Browse": "Araştır",
|
||||
"Browse publicly shared groups and files below.": "Herkese açık paylaşılan grup ve dosyalara aşağıdan göz atın.",
|
||||
"Built-in": "Yerleşik",
|
||||
"Built-in role names cannot be changed.": "Yerleşik rollerin adı değiştirilemez.",
|
||||
"Built-in roles cannot be deleted.": "Yerleşik roller silinemez.",
|
||||
"Call to action": "Eylem çağrısı",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Parolayı doğrula",
|
||||
"Conflict": "Çakışma",
|
||||
"Connect": "Bağlamak",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Yeni yüklemelerin depolanacağı yer olarak harici bir kova — S3 uyumlu (AWS S3, MinIO, Backblaze) ya da Google Cloud Storage — bağlayın.",
|
||||
"Connection Closed Without Response": "Bağlantı Yanıtsız Kapatıldı",
|
||||
"Connection Timed Out": "Bağlantı Zaman Aşımına Uğradı",
|
||||
"Continue": "Devam Et",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Hesabınızı şimdi devre dışı bırakın; :days gün sonra kalıcı olarak silinir",
|
||||
"Deactivated the account \":subject\"": "\":subject\" hesabını devre dışı bıraktı",
|
||||
"Default": "Varsayılan",
|
||||
"Defaults to the file name.": "Varsayılan olarak dosya adı kullanılır.",
|
||||
"Delete": "Sil",
|
||||
"Delete :name": ":name'u sil",
|
||||
"Delete account": "Hesabı sil",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Reddet",
|
||||
"Deny this request?": "Bu başvuru reddedilsin mi?",
|
||||
"Description": "Açıklama",
|
||||
"Description (optional)": "Açıklama (isteğe bağlı)",
|
||||
"Detach": "Ayırmak",
|
||||
"Details": "Detaylar",
|
||||
"Disable": "Devre Dışı Bırak",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": ":Page sayfasına git",
|
||||
"Go to the login form": "Giriş formuna git",
|
||||
"Gone": "Gitmiş",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Gri",
|
||||
"Green": "Yeşil",
|
||||
"Group": "Grup",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Giriş Yap",
|
||||
"Logout": "Çıkış Yap",
|
||||
"Loop Detected": "Döngü Tespit Edildi",
|
||||
"Mail provider settings saved.": "E-posta sağlayıcı ayarları kaydedildi.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Bakım Modu",
|
||||
"Manage custom fields": "Özel alanları yönetme",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Sizinle henüz hiç dosya paylaşılmadı.",
|
||||
"No files here yet.": "Burada henüz dosya yok.",
|
||||
"No files or folders match your search.": "Aramanıza uyan dosya veya klasör yok.",
|
||||
"No files yet. Upload the first one.": "Henüz dosya yok. İlkini yükleyin.",
|
||||
"No folder": "Klasör yok",
|
||||
"No groups match these filters.": "Bu filtrelere uyan grup yok.",
|
||||
"No groups yet.": "Henüz grup yok.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Henüz alıcı eklenmedi.",
|
||||
"No requests match your search.": "Aramanıza uyan başvuru yok.",
|
||||
"No roles match this filter.": "Bu filtreye uyan rol yok.",
|
||||
"No service account key has been saved yet.": "Henüz bir hizmet hesabı anahtarı kaydedilmedi.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Boyut sınırı yok. Yüklemeler duraklatılabilir ve kesintiden sonra kendiliğinden devam eder.",
|
||||
"No users match these filters.": "Bu filtrelere uyan kullanıcı yok.",
|
||||
"No users yet.": "Henüz kullanıcı yok.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Turuncu",
|
||||
"Origin Is Unreachable": "Kökeni Ulaşılamaz",
|
||||
"Outgoing email": "Giden e-posta",
|
||||
"Outgoing email theme": "Giden e-posta teması",
|
||||
"Page :current of :last": "Sayfa :current / :last",
|
||||
"Page Expired": "Sayfa zaman aşımına uğradı",
|
||||
"Pagination Navigation": "Sayfalandırma Çubuğu",
|
||||
"Partial Content": "Kısmi İçerik",
|
||||
"Password": "Parola",
|
||||
"Password reset": "Parola sıfırlama",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Kovadaki nesneleri okuma ve yazma yetkisi olan bir hizmet hesabının JSON anahtar dosyasını yapıştırın. Şifrelenmiş olarak saklanır ve bir daha gösterilmez.",
|
||||
"Path": "Yol",
|
||||
"Payload Too Large": "Veri Çok Büyük",
|
||||
"Payment Required": "ödeme gerekli",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Herkese açık liste ayarları",
|
||||
"Public listing URL": "Herkese açık liste adresi",
|
||||
"Public pages & portal": "Herkese açık sayfalar ve portal",
|
||||
"Public pages and client portal theme": "Herkese açık sayfaların ve müşteri portalının teması",
|
||||
"Published": "Yayımlandı",
|
||||
"Purple": "Mor",
|
||||
"Quota": "Kota",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Onar",
|
||||
"Restore :name": ":name'u geri yükle",
|
||||
"Restricted extension": "Kısıtlı uzantı",
|
||||
"Result": "Sonuç",
|
||||
"results": "sonuçlar",
|
||||
"Review membership requests": "Üyelik başvurularını inceleme",
|
||||
"Review pending requests": "Bekleyen başvuruları incele",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Rol adı",
|
||||
"Role updated.": "Rol güncellendi.",
|
||||
"Roles": "Roller",
|
||||
"S3-compatible": "S3 uyumlu",
|
||||
"Sample email": "Örnek e-posta",
|
||||
"Save": "Kaydet",
|
||||
"Save :name": ":name'u kaydet",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Kuyruğu atlayarak hemen gerçek bir e-posta gönderir; böylece yukarıdaki ayarların gerçekten çalıştığını doğrulayabilirsiniz.",
|
||||
"Separate paragraphs with a blank line.": "Paragrafları boş bir satırla ayırın.",
|
||||
"Server Error": "Sunucu Hatası",
|
||||
"Service account key": "Hizmet hesabı anahtarı",
|
||||
"Service Unavailable": "Hizmet Kullanılamıyor",
|
||||
"Session Has Expired": "Oturum süresi doldu",
|
||||
"Set file expiration dates": "Dosya son kullanma tarihi belirleme",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Ayarlar",
|
||||
"Settings that apply to the whole installation": "Kurulumun tamamı için geçerli ayarlar",
|
||||
"Setup complete": "Kurulum tamamlandı",
|
||||
"Share a file with the people who manage your account.": "Hesabınızı yöneten kişilerle bir dosya paylaşın.",
|
||||
"Share files with several clients at once": "Dosyaları tek seferde birden fazla müşteriyle paylaşın",
|
||||
"Shared the folder \":subject\" with :target": "\":subject\" klasörünü :target ile paylaştı",
|
||||
"Shared with": "Paylaşıldığı kişiler",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Protokoller Değiştiriliyor",
|
||||
"System": "Sistem",
|
||||
"System Administrator": "Sistem yöneticisi",
|
||||
"System settings": "Sistem ayarları",
|
||||
"System settings were updated": "Sistem ayarları güncellendi",
|
||||
"System users": "Sistem kullanıcıları",
|
||||
"Tag": "Etiket",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Deneme",
|
||||
"Text": "Metin",
|
||||
"Text or author": "Metin veya yazar",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Bu bir hizmet hesabı anahtar dosyasına benzemiyor: geçerli bir JSON değil.",
|
||||
"That service account key file is missing its :field.": "Bu hizmet hesabı anahtar dosyasında :field eksik.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": ":name kişisinin hesabı kalıcı olarak silinecek. Bu işlem geri alınamaz.",
|
||||
"The account password was changed": "Hesap parolası değiştirildi",
|
||||
"The account request of :name will be denied and the account deleted.": ":name kişisinin hesap başvurusu reddedilecek ve hesap silinecek.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Üyesi olduğunuz gruplar ve katılmayı isteyebileceğiniz gruplar",
|
||||
"The header/footer style used for every outgoing notification email.": "Giden her bildirim e-postasında kullanılan üst ve alt bilgi biçimi.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Ziyaretçilere açık sayfaların, müşteri portalının ve giden bildirim e-postalarının görünümü.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Posta sunucusunun kendisi burada değil, bu kurulumun .env dosyasında (MAIL_* değişkenleri) yapılandırılır.",
|
||||
"The people who administer this installation and upload files": "Bu kurulumu yöneten ve dosya yükleyen kişiler",
|
||||
"The people you share files with": "Dosya paylaştığınız kişiler",
|
||||
"The provided two-factor authentication code was invalid.": "Girilen iki adımlı doğrulama kodu geçersizdi.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Henüz yapılandırılacak bir şey yok.",
|
||||
"Test email from :site": ":site sitesinden deneme e-postası",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Bu dosyalar diskten kalıcı olarak kaldırılacak. Bu işlem geri alınamaz.",
|
||||
"Theme": "Tema",
|
||||
"Theme updated.": "Tema güncellendi.",
|
||||
"Theming": "Temalar",
|
||||
"Theming settings": "Tema ayarları",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Bu yükleme, :quota MB'lik depolama kotanızı aşardı.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "İpucu: dosya ve klasörleri taşımak için bir klasörün ya da yol izinin üzerine sürükleyin.",
|
||||
"Title": "Başlık",
|
||||
"Title (optional)": "Başlık (isteğe bağlı)",
|
||||
"TLS": "TLS",
|
||||
"to": "a",
|
||||
"To": "Kime",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Çok fazla dosya seçildi. Daha az dosya seçip yeniden deneyin.",
|
||||
"Too Many Requests": "Çok Fazla İstek",
|
||||
"Transactional email settings for this installation": "Bu kurulumun işlem e-postası ayarları",
|
||||
"Transfers — last 30 days": "Aktarımlar — son 30 gün",
|
||||
"Translate": "Çevirmek",
|
||||
"Translate It": "Çevir",
|
||||
"Turns all transactional email for this installation on or off.": "Bu kurulumun tüm işlem e-postalarını açar veya kapatır.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "\":subject\" dosyasını yükledi",
|
||||
"Uploader": "Yükleyen",
|
||||
"Uploads": "Yüklemeler",
|
||||
"Uploads (chart)": "Yüklemeler (grafik)",
|
||||
"URI Too Long": "URI Çok Uzun",
|
||||
"URL slug": "Adres kısaltması",
|
||||
"Use a custom link instead of a random one": "Rastgele bağlantı yerine özel bir bağlantı kullan",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Yalnızca bu seferlik gösterilecek. Yalnızca bir özet saklıyoruz, bu yüzden sonradan geri alınamaz.",
|
||||
"Copy to clipboard": "Panoya kopyala",
|
||||
"Copied": "Kopyalandı",
|
||||
"Your tokens": "Belirteçleriniz",
|
||||
"Revoking a token takes effect immediately.": "Bir belirtecin iptali hemen geçerli olur.",
|
||||
"You have not created any tokens yet.": "Henüz hiç belirteç oluşturmadınız.",
|
||||
"Expired": "Süresi doldu",
|
||||
"Never used": "Hiç kullanılmadı",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Müşteri özel alanlarını yönetme",
|
||||
"Moderate comments": "Yorumları denetleme",
|
||||
"Only me": "Yalnızca ben",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Bu dosyayı açabilen herkese görünür; giriş yapmamış ziyaretçiler de dahil.",
|
||||
"Anyone, including visitors who are not logged in": "Herkes; giriş yapmamış ziyaretçiler de dahil",
|
||||
"Staff and clients": "Ekip ve müşteriler",
|
||||
"Staff only": "Yalnızca ekip",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Bir dosya yeni yorum almayı bıraksa bile, daha önce yazılan yorumlar okunabilir kalır.",
|
||||
"Comments awaiting approval": "Onay bekleyen yorumlar",
|
||||
"Comment settings": "Yorum ayarları",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Giriş yapmamış ziyaretçilerin bıraktığı yorumlar. Siz onaylayana kadar kimse göremez.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Yeni yüklemelerin depolanacağı yer olarak harici, S3 uyumlu bir kova (AWS S3, MinIO ya da başka bir S3 uyumlu hizmet) bağlayın.",
|
||||
"Custom endpoint": "Özel uç nokta",
|
||||
"Custom field created.": "Özel alan oluşturuldu.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Başarısız iş yok.",
|
||||
"No files match these filters.": "Bu filtrelere uyan dosya yok.",
|
||||
"None of the selected files could be edited.": "Seçilen dosyaların hiçbiri düzenlenemedi.",
|
||||
"Nothing is waiting for approval.": "Onay bekleyen bir şey yok.",
|
||||
"of :count": "/ :count",
|
||||
"of :quota": "/ :quota",
|
||||
"on": "tarihinde",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Sayfa",
|
||||
"Path prefix": "Yol öneki",
|
||||
"Pending": "Bekliyor",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "projectsend.org'un herkese açık deposunda yeni bir sürüm olup olmadığını düzenli olarak denetler ve panelde bir bildirim gösterir. Hiçbir şey kendiliğinden indirilmez veya uygulanmaz.",
|
||||
"Permanently failed": "Kalıcı olarak başarısız",
|
||||
"Post comment": "Yorumu gönder",
|
||||
"Preview of :name": ":name önizlemesi",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "İki adımlı doğrulamayı kaldır",
|
||||
"Remove two-factor authentication?": "İki adımlı doğrulama kaldırılsın mı?",
|
||||
"Require a verified email address": "Doğrulanmış bir e-posta adresi iste",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Bunları web sunucunuzun çalıştığı kullanıcı olarak çalıştırın. Tüm yordam INSTALL.md dosyasındadır.",
|
||||
"Score threshold": "Puan eşiği",
|
||||
"Search timezones…": "Saat dilimlerini ara…",
|
||||
"Security check failed. Please try again.": "Güvenlik kontrolü başarısız oldu. Lütfen yeniden deneyin.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Bu site reCAPTCHA ile korunmaktadır ve Google Gizlilik Politikası ile Hizmet Şartları geçerlidir.",
|
||||
"Those files have reached their download limit.": "Bu dosyalar indirme sınırına ulaştı.",
|
||||
"Timezone": "Saat dilimi",
|
||||
"To update, back up your database and files, then:": "Güncellemek için veritabanınızı ve dosyalarınızı yedekleyin, ardından:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Güncellemek için INSTALL.md dosyasındaki adımları izleyin — önce yedekleyin, ardından sürümü açın ve göçleri çalıştırın.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Bunu yalnızca bu sunucu dışarı istek yapmamalıysa kapatın. Hizmete ulaşılamazsa parola yine de kabul edilir, bu yüzden çevrimdışı bir kurulum bundan dolayı asla engellenmez.",
|
||||
"Two-factor authentication removed.": "İki adımlı doğrulama kaldırıldı.",
|
||||
"Two-factor authentication removed by an admin": "İki adımlı doğrulama bir yönetici tarafından kaldırıldı",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Kendi hesabınız :timezone olarak ayarlı, bu yüzden bu, tarihlerin size nasıl göründüğünü değiştirmez.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "\"common\" değil, kendi kiracınız. Microsoft bir kullanıcının hesabındaki e-posta adresini değiştirmesine izin verdiğinden, bir oturum açma yalnızca belirteç burada belirttiğiniz kiracıdan geldiğinde güvenilirdir.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Sağlayıcınız bu kurulumun gizli anahtarını reddetti, bu yüzden korunan formlar denetlenmeden geçiriliyor. Aşağıdaki gizli anahtarı kontrol edin.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Herkese açık formlarınız korunuyor. İstediğiniz zaman kendi anahtarlarınıza geçebilirsiniz."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Herkese açık formlarınız korunuyor. İstediğiniz zaman kendi anahtarlarınıza geçebilirsiniz.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Bilerek eski sürüme döndüyseniz, veritabanının geri yüklediğiniz kodla eşleşmesi için php artisan projectsend:update komutunu çalıştırın.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Bir sürümü denetlemeden önce, indirmeden önce ve kurulumunuza dokunmadan önce size sorar; indirdiği şeyin sağlama toplamını da doğrular. Tüm yordam UPDATE.md dosyasındadır.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Yeni dosyalar yerinde, ancak veritabanı yalnızca :applied sürümüne getirildi. Güncelleme çalıştırılana kadar bu kurulum, veritabanıyla eşleşmeyen bir kodu çalıştırıyor.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "projectsend.org'un herkese açık deposunda yeni bir sürüm olup olmadığını düzenli olarak denetler ve panelde bir bildirim gösterir. Hiçbir şey kendiliğinden indirilmez veya uygulanmaz — güncellemeyi her zaman siz başlatırsınız ve güncelleme betiği her adımdan önce sorar.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Veritabanı :applied sürümüne güncellendi, PHP ise hâlâ bundan önce derlediği kodu sunuyor. Yeniden yüklerseniz bu bildirim kaybolur.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Veritabanı :applied sürümüne güncellendi, bu kapsayıcı ise hâlâ başladığı kodu çalıştırıyor.",
|
||||
"This server is running version :running, but version :applied is installed": "Bu sunucu :running sürümünü çalıştırıyor, ancak kurulu olan sürüm :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Güncellemek için kurulum dizininde sudo ./update.sh komutunu çalıştırın — bir şey indirmeden veya değiştirmeden önce size sorar.",
|
||||
"To update, run this in the install directory:": "Güncellemek için kurulum dizininde şunu çalıştırın:",
|
||||
"Version :running is installed, but the update has not been run": ":running sürümü kurulu, ancak güncelleme çalıştırılmadı",
|
||||
"Updated ProjectSend to :to, from :from": "ProjectSend'i :from sürümünden :to sürümüne güncelledi",
|
||||
"ProjectSend was updated to a new version": "ProjectSend yeni bir sürüme güncellendi",
|
||||
"Come and say hello": "Gelin merhaba deyin",
|
||||
"Continue to the dashboard": "Panele devam edin",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Discord'a katılın",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend'in bir Discord'u var: sürüm haberleri, bir şey beklendiği gibi çalışmadığında yardım ve aynı yazılımı kullanan diğer insanlar. Biz de oradayız.",
|
||||
"What's new": "Yenilikler",
|
||||
"What each release brought to this installation.": "Her sürümün bu kuruluma neler getirdiği.",
|
||||
"Add the rest of your team": "Ekibinizin geri kalanını ekleyin",
|
||||
"Add your first client": "İlk müşterinizi ekleyin",
|
||||
"Check the scheduler is running": "Zamanlayıcının çalıştığını kontrol edin",
|
||||
"Choose how your email looks": "E-postalarınızın görünümünü seçin",
|
||||
"Choose how your file lists look": "Dosya listelerinizin görünümünü seçin",
|
||||
"Drop one in and choose who gets it.": "Bir dosya bırakın ve kimin alacağını seçin.",
|
||||
"Essential": "Vazgeçilmez",
|
||||
"Expiring files and queued email need it.": "Süresi dolan dosyalar ve sıradaki e-postalar buna bağlı.",
|
||||
"Four layouts for the client-facing pages.": "Müşterilerin gördüğü sayfalar için dört düzen.",
|
||||
"Getting started": "Başlarken",
|
||||
"Group your clients": "Müşterilerinizi gruplayın",
|
||||
"Made with care in Argentina. Shared with the world.": "Arjantin'de özenle yapıldı. Dünyayla paylaşıldı.",
|
||||
"Previewed on a real message.": "Gerçek bir mesaj üzerinde önizlenir.",
|
||||
"Resets and share links go out through it.": "Parola sıfırlamaları ve paylaşım bağlantıları oradan çıkar.",
|
||||
"Set up your mail server": "Posta sunucunuzu ayarlayın",
|
||||
"Share once instead of six times.": "Altı kez yerine bir kez paylaşın.",
|
||||
"Staff accounts, each scoped by its role.": "Her biri rolüyle sınırlı ekip hesapları.",
|
||||
"Thank you for installing ProjectSend": "ProjectSend'i kurduğunuz için teşekkürler",
|
||||
"Thank you for updating ProjectSend": "ProjectSend'i güncellediğiniz için teşekkürler",
|
||||
"The people you send files to.": "Dosya gönderdiğiniz kişiler.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Yeni bir kurulumda yapılacakların kısa hâli. Bunların hiçbiri zaman aşımına uğramaz.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Artık :version sürümündesiniz ve her şey yeniden çalışıyor. Dosya paylaşımınız için ProjectSend'e güvenmeye devam ettiğiniz için teşekkürler.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Artık :version sürümündesiniz, :previous sürümünden geldiniz ve her şey yeniden çalışıyor. Dosya paylaşımınız için ProjectSend'e güvenmeye devam ettiğiniz için teşekkürler.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": ":version sürümünü kullanıyorsunuz ve o sizin. İlk yapmaya değecek şeyler burada.",
|
||||
"Thank you for choosing ProjectSend": "ProjectSend'i seçtiğiniz için teşekkürler",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Siteniz hazır, çalışır durumda tutmak da bizim işimiz. İlk yapmaya değecek şeyler burada.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 her şeyi saklar. Okunmamış bildirimler, ne kadar eski olursa olsun asla silinmez.",
|
||||
":version is available": ":version sürümü mevcut",
|
||||
":version on :date": ":version, :date",
|
||||
"Check now": "Şimdi denetle",
|
||||
"Checked a moment ago, so this is the answer from then.": "Denetim az önce yapıldı, dolayısıyla bu o andaki yanıt.",
|
||||
"Checking…": "Denetleniyor…",
|
||||
"Could not reach the update feed.": "Güncelleme akışına ulaşılamadı.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Herhangi bir şeye dokunmadan önce veritabanının dökümünü al ve döküm başarısız olursa dur.",
|
||||
"Housekeeping": "Bakım",
|
||||
"Keep failed jobs for": "Başarısız işleri saklama süresi",
|
||||
"Keep read notifications for": "Okunmuş bildirimleri saklama süresi",
|
||||
"Last checked :date": "Son denetim: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "PHP'ye ve worker'a dokunma; onları kendin yeniden başlatacağın durumlar için.",
|
||||
"Other options": "Diğer seçenekler",
|
||||
"Report which version is installed and which is available, and change nothing.": "Hangi sürümün kurulu, hangisinin mevcut olduğunu bildirir ve hiçbir şeyi değiştirmez.",
|
||||
"Retention updated.": "Saklama süresi güncellendi.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Betiği seçeneksiz çalıştırırsan bunların her birini yol boyunca sorar. Bunlar, yanıtı zaten bildiğin çalıştırmalar içindir.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Yedek sorusunu atlar; başında beklemediğin bir çalıştırma için.",
|
||||
"The latest release (:tag) is not a version this can compare.": "En son sürüm (:tag) burada karşılaştırılabilecek bir sürüm değil.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "İki şey kendiliğinden birikir. Bunlar, yukarıdaki günlük temizliklerin uyduğu sürelerdir.",
|
||||
"Up to date": "Güncel",
|
||||
"Update checks are not available on this edition.": "Güncelleme denetimleri bu sürümde kullanılamaz.",
|
||||
"Update options": "Güncelleme seçenekleri",
|
||||
"Updated": "Güncellendi",
|
||||
"Version :version is available. You are running :current.": ":version sürümü mevcut. Sen :current sürümünü kullanıyorsun.",
|
||||
"You are up to date, running version :current.": "Güncelsin, :current sürümünü kullanıyorsun.",
|
||||
"days": "gün",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help tüm seçenekleri listeler, UPDATE.md ise elle yapmak da dahil olmak üzere tüm prosedürü anlatır.",
|
||||
"Fetch dashboard news": "Panoya haberleri getir",
|
||||
"Purge API request logs": "API istek günlüğünü temizle",
|
||||
"Purge erased accounts": "Silinmiş hesapları temizle",
|
||||
"Purge expired files": "Süresi dolmuş dosyaları temizle",
|
||||
"Purge failed jobs": "Başarısız işleri temizle",
|
||||
"Purge orphan files": "Sahipsiz dosyaları temizle",
|
||||
"Purge read notifications": "Okunmuş bildirimleri temizle",
|
||||
"Purge stale chunked uploads": "Yarım kalmış parçalı yüklemeleri temizle",
|
||||
"Purge zip downloads": "ZIP indirmelerini temizle",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Bu filtreye uyan: :count, en yenisi başta",
|
||||
"A file was previewed via the public group listing": "Bir dosya herkese açık grup listesi üzerinden önizlendi",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Bağlantıya sahip olan herkes bir görseli, videoyu, ses dosyasını veya PDF'yi indirmek yerine tarayıcıda açabilir. Herkese açık bir dosyayı görmenin tek yolu indirmek olsun istiyorsanız bunu kapatın.",
|
||||
"Clients can preview files": "Müşteriler dosyaları önizleyebilir",
|
||||
"Downloads & previews": "İndirmeler ve önizlemeler",
|
||||
"Downloads: :count": "İndirmeler: :count",
|
||||
"Let visitors preview public files": "Ziyaretçiler herkese açık dosyaları önizleyebilsin",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Müşterilerin bir görseli, videoyu, ses dosyasını veya PDF'yi yalnızca indirmek yerine portalda açmasına izin verir. Bir dosyayı görmenin tek yolu indirmek olsun istiyorsanız bunu kapatın. Ekip her zaman önizleyebilir.",
|
||||
"No downloads match these filters.": "Bu filtrelere uyan indirme yok.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Bu dosyayı henüz kimse indirmedi veya önizlemedi.",
|
||||
"Preview :name": ":name dosyasını önizle",
|
||||
"Previewed the file \":subject\" via the public group listing": "\":subject\" dosyasını herkese açık grup listesi üzerinden önizledi",
|
||||
"Previews: :count": "Önizlemeler: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Bu kurulum kendi imajlarını derler, dolayısıyla hazır bir imaj çekmek hiçbir şeyi değiştirmez. Sürüm composer.lock dosyasını veya ön yüzü değiştirdiyse composer install ve npm run build komutlarını da çalıştırın — tüm yordam UPDATE.md içinde.",
|
||||
"To update, run this in the directory you cloned:": "Güncellemek için bunu klonladığınız dizinde çalıştırın:",
|
||||
"View all previews (:count)": "Tüm önizlemeleri gör (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Tarayıcınız bu dosyayı oynatamıyor. Görüntülemek için indirin."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count lượt tải xuống",
|
||||
":count file deleted.|:count files deleted.": "Đã xóa :count tệp.|Đã xóa :count tệp.",
|
||||
":count file imported.|:count files imported.": "Đã nhập :count tệp.|Đã nhập :count tệp.",
|
||||
":count files": ":count tệp",
|
||||
":count folders": ":count thư mục",
|
||||
":count items have been shared with you": ":count mục đã được chia sẻ với bạn",
|
||||
":count members": ":count thành viên",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(và :count lỗi khác)",
|
||||
"(and :count more errors)": "(và :count lỗi khác)|(và :count lỗi khác)|(và :count lỗi khác)",
|
||||
"(deleted account)": "(tài khoản đã xóa)",
|
||||
"0 means unlimited.": "0 nghĩa là không giới hạn.",
|
||||
"A client account request was approved": "Một yêu cầu tài khoản khách hàng đã được duyệt",
|
||||
"A client account request was denied": "Một yêu cầu tài khoản khách hàng đã bị từ chối",
|
||||
"A client left a group": "Một khách hàng đã rời nhóm",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "Đã tạo tài khoản khách hàng mới: :name (:email).",
|
||||
"A new client has registered": "Một khách hàng mới đã đăng ký",
|
||||
"A new ProjectSend version is available": "Đã có phiên bản ProjectSend mới",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "Khi bạn đánh dấu một nhóm là công khai, trang của nhóm luôn truy cập được bằng liên kết riêng — phần này chỉ điều chỉnh địa chỉ gốc và một trang danh mục tùy chọn liệt kê mọi nhóm và tệp công khai cùng lúc.",
|
||||
"A public link was created": "Một liên kết công khai đã được tạo",
|
||||
"A public link was revoked": "Một liên kết công khai đã bị thu hồi",
|
||||
"A role was created": "Một vai trò đã được tạo",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "Quản trị: có yêu cầu tham gia nhóm",
|
||||
"Admin: new client registered": "Quản trị: khách hàng mới đã đăng ký",
|
||||
"Administration": "Quản trị",
|
||||
"After uploading you can assign it to clients and groups": "Sau khi tải lên, bạn có thể gán cho khách hàng và nhóm",
|
||||
"Agree": "Đồng ý",
|
||||
"All": "Tất cả",
|
||||
"All actions": "Tất cả thao tác",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "Tất cả bình luận",
|
||||
"All failed jobs deleted.": "Đã xóa tất cả tác vụ thất bại.",
|
||||
"All files": "Tất cả tệp",
|
||||
"All groups": "Tất cả nhóm",
|
||||
"All permissions": "Tất cả quyền",
|
||||
"All rights reserved.": "Đã đăng kí bản quyền",
|
||||
"All roles": "Tất cả vai trò",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "Người đang dùng liên kết đó sẽ không tải được tệp nữa.",
|
||||
"Appearance": "Giao diện",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "Áp dụng cho các trang nhóm và tệp công khai dành cho khách, cùng cổng khách hàng “Tệp của tôi”.",
|
||||
"Apply filters": "Áp dụng bộ lọc",
|
||||
"Approve": "Duyệt",
|
||||
"Approve account requests": "Duyệt yêu cầu tài khoản",
|
||||
"Approve group membership requests": "Duyệt yêu cầu tham gia nhóm",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "Bạn có chắc muốn xóa tài khoản của mình không?",
|
||||
"Assign": "Giao phó",
|
||||
"Assign categories to files": "Gán danh mục cho tệp",
|
||||
"Assigned": "Đã gán",
|
||||
"Assigned clients": "Khách hàng phụ trách",
|
||||
"Assigned the file \":subject\" to :target": "Đã gán tệp “:subject” cho :target",
|
||||
"Associate": "Kết hợp",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "Tác giả",
|
||||
"Available groups": "Nhóm có thể tham gia",
|
||||
"Available placeholders": "Biến có thể dùng",
|
||||
"Back to My files": "Quay lại Tệp của tôi",
|
||||
"Bad Gateway": "Cổng Không Hợp Lệ",
|
||||
"Bad Request": "Yêu Cầu Không Hợp Lệ",
|
||||
"Bandwidth Limit Exceeded": "Giới Hạn Băng Thông",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "Nội dung",
|
||||
"Browse": "Duyệt qua",
|
||||
"Browse publicly shared groups and files below.": "Xem các nhóm và tệp được chia sẻ công khai bên dưới.",
|
||||
"Built-in": "Có sẵn",
|
||||
"Built-in role names cannot be changed.": "Không thể đổi tên các vai trò có sẵn.",
|
||||
"Built-in roles cannot be deleted.": "Không thể xóa các vai trò có sẵn.",
|
||||
"Call to action": "Nút kêu gọi hành động",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "Xác nhận mật khẩu",
|
||||
"Conflict": "Xung Đột",
|
||||
"Connect": "Kết nối",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "Kết nối một bucket bên ngoài — tương thích S3 (AWS S3, MinIO, Backblaze) hoặc Google Cloud Storage — làm nơi lưu trữ cho các tệp tải lên mới.",
|
||||
"Connection Closed Without Response": "Đóng Kết Nối Với Không Phản Hồi",
|
||||
"Connection Timed Out": "Quá Thời Gian Kết Nối",
|
||||
"Continue": "Tiếp Tục",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "Vô hiệu hóa tài khoản ngay; sau :days ngày sẽ bị xóa vĩnh viễn",
|
||||
"Deactivated the account \":subject\"": "Đã vô hiệu hóa tài khoản “:subject”",
|
||||
"Default": "Mặc định",
|
||||
"Defaults to the file name.": "Mặc định lấy theo tên tệp.",
|
||||
"Delete": "Xóa",
|
||||
"Delete :name": "Xóa :name",
|
||||
"Delete account": "Xóa tài khoản",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "Từ chối",
|
||||
"Deny this request?": "Từ chối yêu cầu này?",
|
||||
"Description": "Mô tả",
|
||||
"Description (optional)": "Mô tả (không bắt buộc)",
|
||||
"Detach": "Gỡ",
|
||||
"Details": "Chi Tiết",
|
||||
"Disable": "Vô hiệu hóa",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "Tới trang :page",
|
||||
"Go to the login form": "Đến trang đăng nhập",
|
||||
"Gone": "Không Còn",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "Xám",
|
||||
"Green": "Xanh lá",
|
||||
"Group": "Nhóm",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "Đăng nhập",
|
||||
"Logout": "Đăng xuất",
|
||||
"Loop Detected": "Phát Hiện Lặp",
|
||||
"Mail provider settings saved.": "Đã lưu cài đặt nhà cung cấp email.",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "Trạng Thái Bảo Trì",
|
||||
"Manage custom fields": "Quản lý trường tùy chỉnh",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "Chưa có tệp nào được chia sẻ với bạn.",
|
||||
"No files here yet.": "Ở đây chưa có tệp nào.",
|
||||
"No files or folders match your search.": "Không có tệp hay thư mục nào khớp với tìm kiếm của bạn.",
|
||||
"No files yet. Upload the first one.": "Chưa có tệp nào. Hãy tải lên tệp đầu tiên.",
|
||||
"No folder": "Không có thư mục",
|
||||
"No groups match these filters.": "Không có nhóm nào khớp với các bộ lọc này.",
|
||||
"No groups yet.": "Chưa có nhóm nào.",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "Chưa thêm người nhận nào.",
|
||||
"No requests match your search.": "Không có yêu cầu nào khớp với tìm kiếm của bạn.",
|
||||
"No roles match this filter.": "Không có vai trò nào khớp với bộ lọc này.",
|
||||
"No service account key has been saved yet.": "Chưa có khóa tài khoản dịch vụ nào được lưu.",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "Không giới hạn kích thước. Có thể tạm dừng tải lên và quá trình sẽ tự tiếp tục sau khi bị gián đoạn.",
|
||||
"No users match these filters.": "Không có người dùng nào khớp với các bộ lọc này.",
|
||||
"No users yet.": "Chưa có người dùng nào.",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "Cam",
|
||||
"Origin Is Unreachable": "Nguồn Gốc Không Chấp Nhận",
|
||||
"Outgoing email": "Email gửi đi",
|
||||
"Outgoing email theme": "Giao diện email gửi đi",
|
||||
"Page :current of :last": "Trang :current trên :last",
|
||||
"Page Expired": "Trang Đã Hết Hạn",
|
||||
"Pagination Navigation": "Điều hướng phân trang",
|
||||
"Partial Content": "Nội Dung Một Phần",
|
||||
"Password": "Mật khẩu",
|
||||
"Password reset": "Đặt lại mật khẩu",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "Dán tệp khóa JSON của một tài khoản dịch vụ có quyền đọc và ghi đối tượng trong bucket. Tệp được lưu ở dạng mã hóa và không bao giờ hiển thị lại.",
|
||||
"Path": "Đường dẫn",
|
||||
"Payload Too Large": "Tải Trọng Quá Lớn",
|
||||
"Payment Required": "yêu cầu thanh toán",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "Cài đặt danh sách công khai",
|
||||
"Public listing URL": "Địa chỉ danh sách công khai",
|
||||
"Public pages & portal": "Trang công khai và cổng thông tin",
|
||||
"Public pages and client portal theme": "Giao diện trang công khai và cổng khách hàng",
|
||||
"Published": "Đã đăng",
|
||||
"Purple": "Tím",
|
||||
"Quota": "Hạn mức",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "Phục Hồi",
|
||||
"Restore :name": "Khôi phục :name",
|
||||
"Restricted extension": "Phần mở rộng bị hạn chế",
|
||||
"Result": "Kết quả",
|
||||
"results": "kết quả",
|
||||
"Review membership requests": "Xem xét yêu cầu tham gia",
|
||||
"Review pending requests": "Xem xét các yêu cầu đang chờ",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "Tên vai trò",
|
||||
"Role updated.": "Đã cập nhật vai trò.",
|
||||
"Roles": "Vai trò",
|
||||
"S3-compatible": "Tương thích S3",
|
||||
"Sample email": "Email mẫu",
|
||||
"Save": "Lưu",
|
||||
"Save :name": "Tiết kiệm :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "Gửi ngay một email thật, bỏ qua hàng đợi, để bạn kiểm chứng các cài đặt phía trên có thực sự hoạt động không.",
|
||||
"Separate paragraphs with a blank line.": "Ngăn cách các đoạn bằng một dòng trống.",
|
||||
"Server Error": "Máy Chủ Gặp Sự Cố",
|
||||
"Service account key": "Khóa tài khoản dịch vụ",
|
||||
"Service Unavailable": "Dịch Vụ Không Khả Dụng",
|
||||
"Session Has Expired": "Phiên Đã Hết Hạn",
|
||||
"Set file expiration dates": "Đặt ngày hết hạn cho tệp",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "Cài đặt",
|
||||
"Settings that apply to the whole installation": "Các cài đặt áp dụng cho toàn bộ bản cài đặt",
|
||||
"Setup complete": "Đã thiết lập xong",
|
||||
"Share a file with the people who manage your account.": "Chia sẻ tệp với những người quản lý tài khoản của bạn.",
|
||||
"Share files with several clients at once": "Chia sẻ tệp với nhiều khách hàng cùng lúc",
|
||||
"Shared the folder \":subject\" with :target": "Đã chia sẻ thư mục “:subject” với :target",
|
||||
"Shared with": "Chia sẻ với",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "Chuyển Đổi Giao Thức",
|
||||
"System": "Hệ thống",
|
||||
"System Administrator": "Quản trị viên hệ thống",
|
||||
"System settings": "Cài đặt hệ thống",
|
||||
"System settings were updated": "Cài đặt hệ thống đã được cập nhật",
|
||||
"System users": "Người dùng hệ thống",
|
||||
"Tag": "Nhãn",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "Thử",
|
||||
"Text": "Văn bản",
|
||||
"Text or author": "Nội dung hoặc tác giả",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "Đây có vẻ không phải là tệp khóa tài khoản dịch vụ: nó không phải JSON hợp lệ.",
|
||||
"That service account key file is missing its :field.": "Tệp khóa tài khoản dịch vụ này thiếu :field.",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": "Tài khoản của :name sẽ bị xóa vĩnh viễn. Không thể hoàn tác.",
|
||||
"The account password was changed": "Mật khẩu tài khoản đã được đổi",
|
||||
"The account request of :name will be denied and the account deleted.": "Yêu cầu tài khoản của :name sẽ bị từ chối và tài khoản bị xóa.",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "Các nhóm bạn đang tham gia và những nhóm bạn có thể xin vào",
|
||||
"The header/footer style used for every outgoing notification email.": "Kiểu đầu trang và chân trang dùng cho mọi email thông báo gửi đi.",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "Diện mạo của các trang công khai dành cho khách, cổng khách hàng và email thông báo gửi đi.",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "Bản thân máy chủ thư được cấu hình trong tệp .env của bản cài đặt này (các biến MAIL_*), không phải ở đây.",
|
||||
"The people who administer this installation and upload files": "Những người quản trị bản cài đặt này và tải tệp lên",
|
||||
"The people you share files with": "Những người bạn chia sẻ tệp",
|
||||
"The provided two-factor authentication code was invalid.": "Mã xác thực hai bước đã nhập không hợp lệ.",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "Chưa có gì để thiết lập.",
|
||||
"Test email from :site": "Email thử từ :site",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "Các tệp này sẽ bị xóa vĩnh viễn khỏi ổ đĩa. Không thể hoàn tác.",
|
||||
"Theme": "Giao diện",
|
||||
"Theme updated.": "Đã cập nhật giao diện.",
|
||||
"Theming": "Giao diện",
|
||||
"Theming settings": "Cài đặt giao diện",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "Lần tải lên này sẽ vượt quá hạn mức lưu trữ :quota MB của bạn.",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "Mẹo: kéo tệp và thư mục thả vào một thư mục hoặc lên thanh đường dẫn để di chuyển chúng.",
|
||||
"Title": "Tiêu đề",
|
||||
"Title (optional)": "Tiêu đề (không bắt buộc)",
|
||||
"TLS": "TLS",
|
||||
"to": "tới",
|
||||
"To": "Đến",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "Bạn đã chọn quá nhiều tệp. Hãy chọn ít hơn rồi thử lại.",
|
||||
"Too Many Requests": "Quá Nhiều Yêu Cầu",
|
||||
"Transactional email settings for this installation": "Cài đặt email giao dịch của bản cài đặt này",
|
||||
"Transfers — last 30 days": "Lượt truyền — 30 ngày qua",
|
||||
"Translate": "Dịch",
|
||||
"Translate It": "Dịch nó",
|
||||
"Turns all transactional email for this installation on or off.": "Bật hoặc tắt toàn bộ email giao dịch của bản cài đặt này.",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "Đã tải lên tệp “:subject”",
|
||||
"Uploader": "Người tải lên",
|
||||
"Uploads": "Lượt tải lên",
|
||||
"Uploads (chart)": "Lượt tải lên (biểu đồ)",
|
||||
"URI Too Long": "URI Quá Dài",
|
||||
"URL slug": "Đường dẫn rút gọn",
|
||||
"Use a custom link instead of a random one": "Dùng liên kết tự đặt thay vì liên kết ngẫu nhiên",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "Đây là lần duy nhất token được hiển thị. Chúng tôi chỉ lưu bản băm nên sau này không thể khôi phục.",
|
||||
"Copy to clipboard": "Sao chép vào bộ nhớ tạm",
|
||||
"Copied": "Đã sao chép",
|
||||
"Your tokens": "Token của bạn",
|
||||
"Revoking a token takes effect immediately.": "Việc thu hồi token có hiệu lực ngay lập tức.",
|
||||
"You have not created any tokens yet.": "Bạn chưa tạo token nào.",
|
||||
"Expired": "Đã hết hạn",
|
||||
"Never used": "Chưa từng dùng",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "Quản lý trường tùy chỉnh của khách hàng",
|
||||
"Moderate comments": "Kiểm duyệt bình luận",
|
||||
"Only me": "Chỉ mình tôi",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "Mọi người mở được tệp này đều thấy, kể cả khách chưa đăng nhập.",
|
||||
"Anyone, including visitors who are not logged in": "Bất kỳ ai, kể cả khách chưa đăng nhập",
|
||||
"Staff and clients": "Nhân sự và khách hàng",
|
||||
"Staff only": "Chỉ nhân sự",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "Các bình luận đã viết vẫn đọc được, ngay cả khi tệp không nhận thêm bình luận mới.",
|
||||
"Comments awaiting approval": "Bình luận đang chờ duyệt",
|
||||
"Comment settings": "Cài đặt bình luận",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "Bình luận do khách chưa đăng nhập để lại. Không ai thấy được cho tới khi bạn duyệt.",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "Kết nối một bucket bên ngoài tương thích S3 (AWS S3, MinIO hoặc dịch vụ tương thích S3 khác) làm nơi lưu trữ cho các tệp tải lên mới.",
|
||||
"Custom endpoint": "Điểm cuối tùy chỉnh",
|
||||
"Custom field created.": "Đã tạo trường tùy chỉnh.",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "Không có tác vụ thất bại nào.",
|
||||
"No files match these filters.": "Không có tệp nào khớp với các bộ lọc này.",
|
||||
"None of the selected files could be edited.": "Không sửa được tệp nào trong số đã chọn.",
|
||||
"Nothing is waiting for approval.": "Không có gì đang chờ duyệt.",
|
||||
"of :count": "trên :count",
|
||||
"of :quota": "trên :quota",
|
||||
"on": "ngày",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "Trang",
|
||||
"Path prefix": "Tiền tố đường dẫn",
|
||||
"Pending": "Đang chờ",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "Định kỳ kiểm tra kho công khai của projectsend.org xem có phiên bản mới không và hiện thông báo trên bảng điều khiển. Hệ thống không bao giờ tự tải về hay tự cài đặt.",
|
||||
"Permanently failed": "Thất bại hoàn toàn",
|
||||
"Post comment": "Đăng bình luận",
|
||||
"Preview of :name": "Xem trước :name",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "Gỡ xác thực hai yếu tố",
|
||||
"Remove two-factor authentication?": "Gỡ xác thực hai yếu tố?",
|
||||
"Require a verified email address": "Yêu cầu một địa chỉ email đã xác minh",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "Chạy các lệnh này với tư cách người dùng mà máy chủ web của bạn đang chạy. INSTALL.md có quy trình đầy đủ.",
|
||||
"Score threshold": "Ngưỡng điểm",
|
||||
"Search timezones…": "Tìm múi giờ…",
|
||||
"Security check failed. Please try again.": "Kiểm tra bảo mật thất bại. Vui lòng thử lại.",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "Trang này được bảo vệ bởi reCAPTCHA và áp dụng Chính sách quyền riêng tư cùng Điều khoản dịch vụ của Google.",
|
||||
"Those files have reached their download limit.": "Các tệp đó đã đạt giới hạn tải xuống.",
|
||||
"Timezone": "Múi giờ",
|
||||
"To update, back up your database and files, then:": "Để cập nhật, hãy sao lưu cơ sở dữ liệu và tệp của bạn, rồi:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "Để cập nhật, hãy làm theo các bước trong INSTALL.md — sao lưu trước, rồi giải nén bản phát hành và chạy các migration.",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "Chỉ tắt tùy chọn này nếu máy chủ này không nên thực hiện yêu cầu đi ra ngoài. Nếu không thể liên hệ dịch vụ, mật khẩu vẫn được chấp nhận, nên một bản cài đặt ngoại tuyến không bao giờ bị chặn bởi nó.",
|
||||
"Two-factor authentication removed.": "Đã gỡ xác thực hai yếu tố.",
|
||||
"Two-factor authentication removed by an admin": "Xác thực hai yếu tố bị gỡ bởi một quản trị viên",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "Tài khoản của chính bạn được đặt là :timezone, nên điều này sẽ không thay đổi cách ngày tháng hiển thị với bạn.",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "Đối tượng thuê của riêng bạn, không phải \"common\". Microsoft cho phép người dùng đổi địa chỉ email trên tài khoản của họ, nên một lần đăng nhập chỉ đáng tin khi token đến từ đối tượng thuê bạn đã nêu ở đây.",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "Nhà cung cấp của bạn đã từ chối khóa bí mật của bản cài đặt này, nên các biểu mẫu được bảo vệ đang được cho qua mà không kiểm tra. Hãy kiểm tra khóa bí mật bên dưới.",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Các biểu mẫu công khai của bạn đã được bảo vệ. Bạn có thể chuyển sang khóa của riêng mình bất cứ lúc nào."
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "Các biểu mẫu công khai của bạn đã được bảo vệ. Bạn có thể chuyển sang khóa của riêng mình bất cứ lúc nào.",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "Nếu bạn cố ý quay lại phiên bản cũ, hãy chạy php artisan projectsend:update để cơ sở dữ liệu khớp với mã bạn đã khôi phục.",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "Nó hỏi bạn trước khi kiểm tra bản phát hành, trước khi tải về và trước khi động đến bản cài đặt của bạn — đồng thời kiểm tra checksum của những gì tải về. UPDATE.md có quy trình đầy đủ.",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "Các tệp mới đã sẵn sàng, nhưng cơ sở dữ liệu mới chỉ được đưa lên :applied. Cho đến khi chạy cập nhật, bản cài đặt này đang chạy mã không khớp với cơ sở dữ liệu của nó.",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "Định kỳ kiểm tra kho công khai của projectsend.org xem có phiên bản mới không và hiện thông báo trên bảng điều khiển. Hệ thống không bao giờ tự tải về hay tự cài đặt — việc cập nhật luôn do bạn chạy, và script cập nhật sẽ hỏi trước mỗi bước.",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "Cơ sở dữ liệu đã được cập nhật lên :applied, còn PHP vẫn phục vụ mã đã biên dịch trước đó. Tải lại PHP là thông báo này biến mất.",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "Cơ sở dữ liệu đã được cập nhật lên :applied, còn container này vẫn chạy mã lúc khởi động.",
|
||||
"This server is running version :running, but version :applied is installed": "Máy chủ này đang chạy phiên bản :running, nhưng phiên bản đã cài là :applied",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "Để cập nhật, hãy chạy sudo ./update.sh trong thư mục cài đặt — nó sẽ hỏi trước khi tải về hay thay đổi bất cứ thứ gì.",
|
||||
"To update, run this in the install directory:": "Để cập nhật, hãy chạy lệnh này trong thư mục cài đặt:",
|
||||
"Version :running is installed, but the update has not been run": "Phiên bản :running đã được cài, nhưng chưa chạy cập nhật",
|
||||
"Updated ProjectSend to :to, from :from": "Đã cập nhật ProjectSend lên :to, từ :from",
|
||||
"ProjectSend was updated to a new version": "ProjectSend đã được cập nhật lên phiên bản mới",
|
||||
"Come and say hello": "Ghé qua chào một tiếng",
|
||||
"Continue to the dashboard": "Tiếp tục đến bảng điều khiển",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "Tham gia Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend có một Discord: tin tức về các bản phát hành, trợ giúp khi có gì đó chạy không đúng, và những người khác đang dùng cùng phần mềm. Chúng tôi cũng ở đó.",
|
||||
"What's new": "Có gì mới",
|
||||
"What each release brought to this installation.": "Mỗi bản phát hành đã mang lại gì cho bản cài đặt này.",
|
||||
"Add the rest of your team": "Thêm những người còn lại trong nhóm",
|
||||
"Add your first client": "Thêm khách hàng đầu tiên",
|
||||
"Check the scheduler is running": "Kiểm tra bộ hẹn giờ có đang chạy không",
|
||||
"Choose how your email looks": "Chọn giao diện email của bạn",
|
||||
"Choose how your file lists look": "Chọn giao diện danh sách tệp",
|
||||
"Drop one in and choose who gets it.": "Thả một tệp vào và chọn ai sẽ nhận.",
|
||||
"Essential": "Thiết yếu",
|
||||
"Expiring files and queued email need it.": "Tệp hết hạn và email đang chờ đều cần nó.",
|
||||
"Four layouts for the client-facing pages.": "Bốn bố cục cho các trang khách hàng nhìn thấy.",
|
||||
"Getting started": "Bắt đầu",
|
||||
"Group your clients": "Nhóm khách hàng của bạn",
|
||||
"Made with care in Argentina. Shared with the world.": "Được làm bằng tâm huyết tại Argentina. Chia sẻ với thế giới.",
|
||||
"Previewed on a real message.": "Xem trước trên một thư thật.",
|
||||
"Resets and share links go out through it.": "Đặt lại mật khẩu và liên kết chia sẻ đều đi qua đó.",
|
||||
"Set up your mail server": "Thiết lập máy chủ thư của bạn",
|
||||
"Share once instead of six times.": "Chia sẻ một lần thay vì sáu lần.",
|
||||
"Staff accounts, each scoped by its role.": "Tài khoản nhân sự, mỗi tài khoản giới hạn theo vai trò.",
|
||||
"Thank you for installing ProjectSend": "Cảm ơn bạn đã cài đặt ProjectSend",
|
||||
"Thank you for updating ProjectSend": "Cảm ơn bạn đã cập nhật ProjectSend",
|
||||
"The people you send files to.": "Những người bạn gửi tệp tới.",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "Bản tóm tắt những việc nên làm trên một bản cài đặt mới. Không có gì ở đây hết hạn.",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Bạn đang dùng :version và mọi thứ đã hoạt động trở lại. Cảm ơn bạn đã tiếp tục tin tưởng ProjectSend cho việc chia sẻ tệp.",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "Bạn đang dùng :version, lên từ :previous, và mọi thứ đã hoạt động trở lại. Cảm ơn bạn đã tiếp tục tin tưởng ProjectSend cho việc chia sẻ tệp.",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "Bạn đang chạy phiên bản :version, và nó là của bạn. Đây là những việc nên làm trước tiên.",
|
||||
"Thank you for choosing ProjectSend": "Cảm ơn bạn đã chọn ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "Trang của bạn đã sẵn sàng, và việc duy trì nó là của chúng tôi. Đây là những việc nên làm trước tiên.",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 giữ lại tất cả. Thông báo chưa đọc không bao giờ bị xóa, dù cũ đến đâu.",
|
||||
":version is available": "Đã có :version",
|
||||
":version on :date": ":version vào :date",
|
||||
"Check now": "Kiểm tra ngay",
|
||||
"Checked a moment ago, so this is the answer from then.": "Việc kiểm tra vừa diễn ra, nên đây là kết quả của lúc đó.",
|
||||
"Checking…": "Đang kiểm tra…",
|
||||
"Could not reach the update feed.": "Không thể kết nối tới nguồn cập nhật.",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "Hãy sao lưu cơ sở dữ liệu trước khi động vào bất cứ thứ gì, và dừng lại nếu việc sao lưu thất bại.",
|
||||
"Housekeeping": "Dọn dẹp",
|
||||
"Keep failed jobs for": "Giữ tác vụ thất bại trong",
|
||||
"Keep read notifications for": "Giữ thông báo đã đọc trong",
|
||||
"Last checked :date": "Lần kiểm tra cuối: :date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "Không đụng đến PHP và worker, dành cho khi bạn tự khởi động lại chúng.",
|
||||
"Other options": "Tùy chọn khác",
|
||||
"Report which version is installed and which is available, and change nothing.": "Báo cáo phiên bản đang cài và phiên bản có sẵn, không thay đổi gì.",
|
||||
"Retention updated.": "Đã cập nhật thời gian lưu giữ.",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "Chạy tập lệnh mà không có tùy chọn thì nó sẽ hỏi từng mục này trong quá trình chạy. Những tùy chọn này dành cho các lần chạy mà bạn đã biết câu trả lời.",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "Bỏ qua câu hỏi về sao lưu, dành cho lần chạy mà bạn không ngồi theo dõi.",
|
||||
"The latest release (:tag) is not a version this can compare.": "Bản phát hành mới nhất (:tag) không phải là phiên bản có thể so sánh ở đây.",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "Có hai thứ tự tích tụ. Đây là các khoảng thời gian mà việc dọn dẹp hằng ngày ở trên tuân theo.",
|
||||
"Up to date": "Đã là mới nhất",
|
||||
"Update checks are not available on this edition.": "Bản này không hỗ trợ kiểm tra cập nhật.",
|
||||
"Update options": "Tùy chọn cập nhật",
|
||||
"Updated": "Đã cập nhật",
|
||||
"Version :version is available. You are running :current.": "Phiên bản :version đã có. Bạn đang dùng :current.",
|
||||
"You are up to date, running version :current.": "Bạn đang dùng phiên bản mới nhất :current.",
|
||||
"days": "ngày",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help liệt kê mọi tùy chọn, còn UPDATE.md hướng dẫn toàn bộ quy trình, kể cả làm thủ công.",
|
||||
"Fetch dashboard news": "Lấy tin tức cho bảng điều khiển",
|
||||
"Purge API request logs": "Dọn nhật ký yêu cầu API",
|
||||
"Purge erased accounts": "Dọn các tài khoản đã xóa hẳn",
|
||||
"Purge expired files": "Dọn các tệp đã hết hạn",
|
||||
"Purge failed jobs": "Dọn các tác vụ thất bại",
|
||||
"Purge orphan files": "Dọn các tệp mồ côi",
|
||||
"Purge read notifications": "Dọn các thông báo đã đọc",
|
||||
"Purge stale chunked uploads": "Dọn các lần tải lên theo phần bị bỏ dở",
|
||||
"Purge zip downloads": "Dọn các tệp ZIP đã tải xuống",
|
||||
":action (:count)": ":action (:count)",
|
||||
":count matching this filter, newest first": "Khớp với bộ lọc này: :count, mới nhất trước",
|
||||
"A file was previewed via the public group listing": "Một tệp đã được xem trước qua danh sách nhóm công khai",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "Bất kỳ ai có liên kết đều có thể mở hình ảnh, video, tệp âm thanh hoặc PDF ngay trong trình duyệt thay vì tải xuống. Hãy tắt tuỳ chọn này nếu tải xuống phải là cách duy nhất để xem tệp công khai.",
|
||||
"Clients can preview files": "Khách hàng có thể xem trước tệp",
|
||||
"Downloads & previews": "Lượt tải xuống và xem trước",
|
||||
"Downloads: :count": "Lượt tải xuống: :count",
|
||||
"Let visitors preview public files": "Cho phép khách truy cập xem trước tệp công khai",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "Cho phép khách hàng mở hình ảnh, video, tệp âm thanh hoặc PDF ngay trong cổng thông tin thay vì chỉ tải xuống. Hãy tắt tuỳ chọn này nếu tải xuống phải là cách duy nhất để xem tệp. Nhân sự luôn có thể xem trước.",
|
||||
"No downloads match these filters.": "Không có lượt tải xuống nào khớp với các bộ lọc này.",
|
||||
"Nobody has downloaded or previewed this file yet.": "Chưa có ai tải xuống hay xem trước tệp này.",
|
||||
"Preview :name": "Xem trước :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "Đã xem trước tệp “:subject” qua danh sách nhóm công khai",
|
||||
"Previews: :count": "Lượt xem trước: :count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "Bản cài đặt này tự dựng image của mình, nên kéo image về cũng không thay đổi gì. Nếu bản phát hành có đụng tới composer.lock hoặc phần giao diện, hãy chạy thêm composer install và npm run build — UPDATE.md có quy trình đầy đủ.",
|
||||
"To update, run this in the directory you cloned:": "Để cập nhật, hãy chạy lệnh này trong thư mục bạn đã sao chép:",
|
||||
"View all previews (:count)": "Xem tất cả lượt xem trước (:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "Trình duyệt của bạn không phát được tệp này. Hãy tải xuống để xem."
|
||||
}
|
||||
|
||||
+110
-34
@@ -5,7 +5,6 @@
|
||||
":count downloads": ":count 次下载",
|
||||
":count file deleted.|:count files deleted.": "已删除 :count 个文件。|已删除 :count 个文件。",
|
||||
":count file imported.|:count files imported.": "已导入 :count 个文件。|已导入 :count 个文件。",
|
||||
":count files": ":count 个文件",
|
||||
":count folders": ":count 个文件夹",
|
||||
":count items have been shared with you": "有 :count 个项目已共享给你",
|
||||
":count members": ":count 位成员",
|
||||
@@ -20,7 +19,6 @@
|
||||
"(and :count more error)": "(还有 :count 个错误)",
|
||||
"(and :count more errors)": "(还有 :count 个错误)",
|
||||
"(deleted account)": "(已删除的账户)",
|
||||
"0 means unlimited.": "0 表示不限。",
|
||||
"A client account request was approved": "客户账户申请已通过",
|
||||
"A client account request was denied": "客户账户申请已拒绝",
|
||||
"A client left a group": "客户退出了群组",
|
||||
@@ -57,7 +55,6 @@
|
||||
"A new client account was created: :name (:email).": "已创建新的客户账户::name(:email)。",
|
||||
"A new client has registered": "有新客户完成注册",
|
||||
"A new ProjectSend version is available": "ProjectSend 有新版本可用",
|
||||
"A public group's page is always reachable by its own link once you mark it public — this section only controls the base URL and an optional directory listing every public group and file at once.": "群组一旦标记为公开,其页面始终可通过专属链接访问——此处仅用于设置基础网址,以及一个可选的目录页,把所有公开群组和文件集中列出。",
|
||||
"A public link was created": "已创建公开链接",
|
||||
"A public link was revoked": "公开链接已吊销",
|
||||
"A role was created": "角色已创建",
|
||||
@@ -99,7 +96,6 @@
|
||||
"Admin: group membership requested": "管理:收到群组成员申请",
|
||||
"Admin: new client registered": "管理:新客户完成注册",
|
||||
"Administration": "管理",
|
||||
"After uploading you can assign it to clients and groups": "上传后可分配给客户和群组",
|
||||
"Agree": "同意",
|
||||
"All": "全部",
|
||||
"All actions": "所有操作",
|
||||
@@ -108,7 +104,6 @@
|
||||
"All comments": "所有评论",
|
||||
"All failed jobs deleted.": "已删除所有失败的任务。",
|
||||
"All files": "所有文件",
|
||||
"All groups": "所有群组",
|
||||
"All permissions": "所有权限",
|
||||
"All rights reserved.": "版权所有。",
|
||||
"All roles": "所有角色",
|
||||
@@ -136,7 +131,6 @@
|
||||
"Anyone using it will no longer be able to download the file.": "正在使用它的人将无法再下载该文件。",
|
||||
"Appearance": "外观",
|
||||
"Applies to the guest-facing public group/file pages and the \"My files\" client portal.": "适用于面向访客的公开群组和文件页面,以及“我的文件”客户门户。",
|
||||
"Apply filters": "应用筛选",
|
||||
"Approve": "通过",
|
||||
"Approve account requests": "审批账户申请",
|
||||
"Approve group membership requests": "审批群组成员申请",
|
||||
@@ -148,7 +142,6 @@
|
||||
"Are you sure you want to delete your account?": "确定要删除你的账户吗?",
|
||||
"Assign": "分配",
|
||||
"Assign categories to files": "为文件分配分类",
|
||||
"Assigned": "已分配",
|
||||
"Assigned clients": "负责的客户",
|
||||
"Assigned the file \":subject\" to :target": "把文件“:subject”分配给了 :target",
|
||||
"Associate": "联系",
|
||||
@@ -159,7 +152,6 @@
|
||||
"Author": "作者",
|
||||
"Available groups": "可加入的群组",
|
||||
"Available placeholders": "可用占位符",
|
||||
"Back to My files": "返回“我的文件”",
|
||||
"Bad Gateway": "网关错误",
|
||||
"Bad Request": "请求错误",
|
||||
"Bandwidth Limit Exceeded": "超出带宽限制",
|
||||
@@ -167,7 +159,6 @@
|
||||
"Body": "正文",
|
||||
"Browse": "浏览",
|
||||
"Browse publicly shared groups and files below.": "在下方浏览公开共享的群组和文件。",
|
||||
"Built-in": "内置",
|
||||
"Built-in role names cannot be changed.": "内置角色的名称无法修改。",
|
||||
"Built-in roles cannot be deleted.": "内置角色无法删除。",
|
||||
"Call to action": "行动按钮",
|
||||
@@ -213,6 +204,7 @@
|
||||
"Confirm password": "确认密码",
|
||||
"Conflict": "冲突",
|
||||
"Connect": "连接",
|
||||
"Connect an external bucket — S3-compatible (AWS S3, MinIO, Backblaze) or Google Cloud Storage — as the storage backend for new uploads.": "连接外部存储桶(S3 兼容 — AWS S3、MinIO、Backblaze — 或 Google Cloud Storage),作为新上传文件的存储后端。",
|
||||
"Connection Closed Without Response": "连接关闭无响应",
|
||||
"Connection Timed Out": "连接超时",
|
||||
"Continue": "继续请求",
|
||||
@@ -265,7 +257,6 @@
|
||||
"Deactivate your account now; it is permanently erased after :days days": "立即停用你的账户;:days 天后将被彻底抹除",
|
||||
"Deactivated the account \":subject\"": "停用了账户“:subject”",
|
||||
"Default": "默认",
|
||||
"Defaults to the file name.": "默认使用文件名。",
|
||||
"Delete": "删除",
|
||||
"Delete :name": "删除 :name 个",
|
||||
"Delete account": "删除账户",
|
||||
@@ -311,7 +302,6 @@
|
||||
"Deny": "拒绝",
|
||||
"Deny this request?": "要拒绝此申请吗?",
|
||||
"Description": "说明",
|
||||
"Description (optional)": "说明(可选)",
|
||||
"Detach": "分离",
|
||||
"Details": "详情",
|
||||
"Disable": "禁用",
|
||||
@@ -421,6 +411,7 @@
|
||||
"Go to page :page": "前往第 :page 页",
|
||||
"Go to the login form": "前往登录页",
|
||||
"Gone": "不可用",
|
||||
"Google Cloud Storage": "Google Cloud Storage",
|
||||
"Gray": "灰色",
|
||||
"Green": "绿色",
|
||||
"Group": "群组",
|
||||
@@ -503,7 +494,6 @@
|
||||
"Login": "登录",
|
||||
"Logout": "登出",
|
||||
"Loop Detected": "检测到环路",
|
||||
"Mail provider settings saved.": "邮件服务商设置已保存。",
|
||||
"Mailgun": "Mailgun",
|
||||
"Maintenance Mode": "服务不可用",
|
||||
"Manage custom fields": "管理自定义字段",
|
||||
@@ -572,7 +562,6 @@
|
||||
"No files have been shared with you yet.": "目前还没有文件共享给你。",
|
||||
"No files here yet.": "这里还没有文件。",
|
||||
"No files or folders match your search.": "没有符合搜索条件的文件或文件夹。",
|
||||
"No files yet. Upload the first one.": "还没有文件,上传第一个吧。",
|
||||
"No folder": "无文件夹",
|
||||
"No groups match these filters.": "没有符合这些筛选条件的群组。",
|
||||
"No groups yet.": "还没有群组。",
|
||||
@@ -587,6 +576,7 @@
|
||||
"No recipients added yet.": "还没有添加收件人。",
|
||||
"No requests match your search.": "没有符合搜索条件的申请。",
|
||||
"No roles match this filter.": "没有符合此筛选条件的角色。",
|
||||
"No service account key has been saved yet.": "尚未保存任何服务账号密钥。",
|
||||
"No size limit. Uploads can be paused and resume automatically after interruptions.": "不限文件大小。上传可以暂停,中断后会自动续传。",
|
||||
"No users match these filters.": "没有符合这些筛选条件的用户。",
|
||||
"No users yet.": "还没有用户。",
|
||||
@@ -625,13 +615,12 @@
|
||||
"Orange": "橙色",
|
||||
"Origin Is Unreachable": "原点无法到达",
|
||||
"Outgoing email": "发出的邮件",
|
||||
"Outgoing email theme": "发出邮件的主题",
|
||||
"Page :current of :last": "第 :current 页,共 :last 页",
|
||||
"Page Expired": "页面会话已超时",
|
||||
"Pagination Navigation": "分页导航",
|
||||
"Partial Content": "部分内容",
|
||||
"Password": "密码",
|
||||
"Password reset": "重置密码",
|
||||
"Paste the JSON key file for a service account with object read and write access to the bucket. It is stored encrypted and never shown again.": "粘贴对存储桶内对象拥有读写权限的服务账号 JSON 密钥文件。它会加密保存,并且不会再次显示。",
|
||||
"Path": "路径",
|
||||
"Payload Too Large": "请求实体过大",
|
||||
"Payment Required": "需要付款",
|
||||
@@ -678,7 +667,6 @@
|
||||
"Public listing settings": "公开列表设置",
|
||||
"Public listing URL": "公开列表网址",
|
||||
"Public pages & portal": "公开页面与门户",
|
||||
"Public pages and client portal theme": "公开页面和客户门户的主题",
|
||||
"Published": "发布时间",
|
||||
"Purple": "紫色",
|
||||
"Quota": "配额",
|
||||
@@ -726,7 +714,6 @@
|
||||
"Restore": "恢复",
|
||||
"Restore :name": "恢复:name",
|
||||
"Restricted extension": "受限扩展名",
|
||||
"Result": "结果",
|
||||
"results": "结果",
|
||||
"Review membership requests": "审核成员申请",
|
||||
"Review pending requests": "查看待处理的申请",
|
||||
@@ -741,6 +728,7 @@
|
||||
"Role name": "角色名称",
|
||||
"Role updated.": "角色已更新。",
|
||||
"Roles": "角色",
|
||||
"S3-compatible": "S3 兼容",
|
||||
"Sample email": "示例邮件",
|
||||
"Save": "保存",
|
||||
"Save :name": "保存 :name",
|
||||
@@ -780,6 +768,7 @@
|
||||
"Sends a real email right now, bypassing the queue, so you can verify the settings above actually work.": "立即绕过队列发送一封真实邮件,便于你确认上面的设置确实可用。",
|
||||
"Separate paragraphs with a blank line.": "段落之间用空行分隔。",
|
||||
"Server Error": "服务器错误",
|
||||
"Service account key": "服务账号密钥",
|
||||
"Service Unavailable": "服务不可用",
|
||||
"Session Has Expired": "会话已过期",
|
||||
"Set file expiration dates": "设置文件到期日期",
|
||||
@@ -789,7 +778,6 @@
|
||||
"Settings": "设置",
|
||||
"Settings that apply to the whole installation": "适用于整个站点的设置",
|
||||
"Setup complete": "设置完成",
|
||||
"Share a file with the people who manage your account.": "把文件共享给管理你账户的人。",
|
||||
"Share files with several clients at once": "一次把文件共享给多个客户",
|
||||
"Shared the folder \":subject\" with :target": "把文件夹“:subject”共享给了 :target",
|
||||
"Shared with": "共享给",
|
||||
@@ -837,7 +825,6 @@
|
||||
"Switching Protocols": "切换协议",
|
||||
"System": "系统",
|
||||
"System Administrator": "系统管理员",
|
||||
"System settings": "系统设置",
|
||||
"System settings were updated": "系统设置已更新",
|
||||
"System users": "系统用户",
|
||||
"Tag": "标签",
|
||||
@@ -846,6 +833,8 @@
|
||||
"Test": "测试",
|
||||
"Text": "文本",
|
||||
"Text or author": "内容或作者",
|
||||
"That does not look like a service account key file: it is not valid JSON.": "这看起来不是服务账号密钥文件:它不是有效的 JSON。",
|
||||
"That service account key file is missing its :field.": "该服务账号密钥文件缺少 :field。",
|
||||
"The account of :name will be permanently deleted. This cannot be undone.": ":name 的账户将被永久删除,此操作无法撤销。",
|
||||
"The account password was changed": "账户密码已修改",
|
||||
"The account request of :name will be denied and the account deleted.": ":name 的账户申请将被拒绝,账户也会被删除。",
|
||||
@@ -874,7 +863,6 @@
|
||||
"The groups you belong to and the ones you can ask to join": "你所在的群组,以及你可以申请加入的群组",
|
||||
"The header/footer style used for every outgoing notification email.": "每封发出的通知邮件所使用的页眉和页脚样式。",
|
||||
"The look of the guest public pages, the client portal, and outgoing notification emails.": "访客公开页面、客户门户以及发出的通知邮件的外观。",
|
||||
"The mail server itself is configured in this installation’s .env file (MAIL_* variables), not here.": "邮件服务器本身在本站点的 .env 文件中配置(MAIL_* 变量),而不是在这里。",
|
||||
"The people who administer this installation and upload files": "管理本站点并上传文件的人",
|
||||
"The people you share files with": "与你共享文件的人",
|
||||
"The provided two-factor authentication code was invalid.": "输入的两步验证码无效。",
|
||||
@@ -888,7 +876,6 @@
|
||||
"There is nothing to configure yet.": "暂时没有可配置的内容。",
|
||||
"Test email from :site": "来自 :site 的测试邮件",
|
||||
"These files will be permanently removed from disk. This cannot be undone.": "这些文件将从磁盘上永久删除,此操作无法撤销。",
|
||||
"Theme": "主题",
|
||||
"Theme updated.": "主题已更新。",
|
||||
"Theming": "主题",
|
||||
"Theming settings": "主题设置",
|
||||
@@ -912,7 +899,6 @@
|
||||
"This upload would exceed your storage quota of :quota MB.": "此次上传会超出你 :quota MB 的存储配额。",
|
||||
"Tip: drag files and folders onto a folder or a breadcrumb to move them.": "提示:把文件和文件夹拖到某个文件夹或面包屑导航上即可移动。",
|
||||
"Title": "标题",
|
||||
"Title (optional)": "标题(可选)",
|
||||
"TLS": "TLS",
|
||||
"to": "至",
|
||||
"To": "收件人",
|
||||
@@ -921,7 +907,6 @@
|
||||
"Too many files selected. Choose a smaller selection and try again.": "选择的文件过多,请减少数量后重试。",
|
||||
"Too Many Requests": "请求次数过多。",
|
||||
"Transactional email settings for this installation": "本站点的事务邮件设置",
|
||||
"Transfers — last 30 days": "传输量——最近 30 天",
|
||||
"Translate": "翻译",
|
||||
"Translate It": "翻译它",
|
||||
"Turns all transactional email for this installation on or off.": "统一开启或关闭本站点的所有事务邮件。",
|
||||
@@ -962,7 +947,6 @@
|
||||
"Uploaded the file \":subject\"": "上传了文件“:subject”",
|
||||
"Uploader": "上传者",
|
||||
"Uploads": "上传",
|
||||
"Uploads (chart)": "上传(图表)",
|
||||
"URI Too Long": "URI太长了",
|
||||
"URL slug": "网址别名",
|
||||
"Use a custom link instead of a random one": "使用自定义链接,而非随机链接",
|
||||
@@ -1064,8 +1048,6 @@
|
||||
"This is the only time it will be shown. We store only a hash, so it cannot be recovered later.": "令牌仅在此显示一次。我们只保存哈希值,之后无法找回。",
|
||||
"Copy to clipboard": "复制到剪贴板",
|
||||
"Copied": "已复制",
|
||||
"Your tokens": "你的令牌",
|
||||
"Revoking a token takes effect immediately.": "吊销令牌会立即生效。",
|
||||
"You have not created any tokens yet.": "你还没有创建任何令牌。",
|
||||
"Expired": "已过期",
|
||||
"Never used": "从未使用",
|
||||
@@ -1185,7 +1167,6 @@
|
||||
"Manage client custom fields": "管理客户自定义字段",
|
||||
"Moderate comments": "审核评论",
|
||||
"Only me": "仅自己",
|
||||
"Visible to anyone who can open this file, including visitors who are not logged in.": "所有能打开此文件的人都可见,包括未登录的访客。",
|
||||
"Anyone, including visitors who are not logged in": "任何人,包括未登录的访客",
|
||||
"Staff and clients": "团队和客户",
|
||||
"Staff only": "仅团队",
|
||||
@@ -1257,7 +1238,6 @@
|
||||
"Comments already written stay readable even if a file stops accepting new ones.": "即使文件不再接受新评论,已发表的评论仍可阅读。",
|
||||
"Comments awaiting approval": "待审核的评论",
|
||||
"Comment settings": "评论设置",
|
||||
"Comments left by visitors who are not logged in. Nobody can see them until you approve them.": "未登录访客发表的评论。在你通过审核前,任何人都看不到。",
|
||||
"Connect an external S3-compatible bucket (AWS S3, MinIO, or another S3-compatible service) as the storage backend for new uploads.": "连接外部的 S3 兼容存储桶(AWS S3、MinIO 或其他 S3 兼容服务),作为新上传文件的存储后端。",
|
||||
"Custom endpoint": "自定义接入点",
|
||||
"Custom field created.": "自定义字段已创建。",
|
||||
@@ -1328,7 +1308,6 @@
|
||||
"No failed jobs.": "没有失败的任务。",
|
||||
"No files match these filters.": "没有符合这些筛选条件的文件。",
|
||||
"None of the selected files could be edited.": "所选文件均无法编辑。",
|
||||
"Nothing is waiting for approval.": "没有待审核的内容。",
|
||||
"of :count": "/ :count",
|
||||
"of :quota": "/ :quota",
|
||||
"on": "于",
|
||||
@@ -1342,7 +1321,6 @@
|
||||
"Page": "页",
|
||||
"Path prefix": "路径前缀",
|
||||
"Pending": "待处理",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically.": "定期检查 projectsend.org 的公开仓库是否有新版本,并在仪表板上给出提示。系统不会自动下载或安装任何内容。",
|
||||
"Permanently failed": "彻底失败",
|
||||
"Post comment": "发表评论",
|
||||
"Preview of :name": ":name 的预览",
|
||||
@@ -1730,7 +1708,6 @@
|
||||
"Remove two-factor authentication": "移除两步验证",
|
||||
"Remove two-factor authentication?": "移除两步验证?",
|
||||
"Require a verified email address": "要求经过验证的电子邮件地址",
|
||||
"Run these as the user your web server runs as. INSTALL.md has the full procedure.": "以你的 Web 服务器所用的用户身份运行这些命令。完整步骤见 INSTALL.md。",
|
||||
"Score threshold": "分数阈值",
|
||||
"Search timezones…": "搜索时区…",
|
||||
"Security check failed. Please try again.": "安全检查失败。请重试。",
|
||||
@@ -1763,8 +1740,6 @@
|
||||
"This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.": "本站点受 reCAPTCHA 保护,并适用 Google 隐私政策和服务条款。",
|
||||
"Those files have reached their download limit.": "这些文件已达到其下载上限。",
|
||||
"Timezone": "时区",
|
||||
"To update, back up your database and files, then:": "如需更新,请备份你的数据库和文件,然后:",
|
||||
"To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.": "如需更新,请按照 INSTALL.md 中的步骤——先备份,然后解压发行版并运行迁移。",
|
||||
"Turn this off only if this server should make no outbound requests. If the service cannot be reached the password is accepted anyway, so an offline installation is never blocked by it.": "仅当此服务器不应发出任何出站请求时才关闭此项。如果无法连接该服务,密码仍会被接受,因此离线安装永远不会因此被阻止。",
|
||||
"Two-factor authentication removed.": "已移除两步验证。",
|
||||
"Two-factor authentication removed by an admin": "两步验证已被管理员移除",
|
||||
@@ -1782,5 +1757,106 @@
|
||||
"Your own account is set to :timezone, so this will not change how dates look to you.": "你自己的账户设置为 :timezone,因此这不会改变日期对你显示的方式。",
|
||||
"Your own tenant, not \"common\". Microsoft lets a user change the email address on their account, so a sign-in is only trustworthy when the token came from the tenant you named here.": "你自己的租户,而不是“common”。Microsoft 允许用户更改其账户上的电子邮件地址,因此只有当令牌来自你在此指定的租户时,登录才是可信的。",
|
||||
"Your provider rejected this installation’s secret key, so protected forms are being let through unchecked. Check the secret key below.": "你的提供方拒绝了此安装的密钥,因此受保护的表单正被不加检查地放行。请检查下方的密钥。",
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "你的公开表单已受到保护。你可以随时切换到自己的密钥。"
|
||||
"Your public forms are protected. You can switch to your own keys at any time.": "你的公开表单已受到保护。你可以随时切换到自己的密钥。",
|
||||
"If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.": "如果你是有意回退版本,请执行 php artisan projectsend:update,让数据库与你恢复的代码保持一致。",
|
||||
"It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.": "在查找新版本之前、下载之前以及改动你的安装之前,它都会先征求同意,并会校验所下载文件的校验和。完整步骤见 UPDATE.md。",
|
||||
"New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.": "新文件已就位,但数据库只升级到了 :applied。在运行更新之前,此安装运行的代码与其数据库并不匹配。",
|
||||
"Periodically checks projectsend.org's public repository for a newer release and shows a notice on the dashboard. Nothing is ever downloaded or applied automatically — updating is always something you run, and the update script asks before each step.": "定期检查 projectsend.org 的公开仓库是否有新版本,并在仪表板上给出提示。系统不会自动下载或安装任何内容——更新始终由你来执行,而且更新脚本在每一步之前都会询问。",
|
||||
"The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.": "数据库已更新到 :applied,而 PHP 仍在提供此前编译的代码。重新加载后此提示即会消失。",
|
||||
"The database was updated to :applied, and this container is still running the code it started with.": "数据库已更新到 :applied,而此容器仍在运行启动时的代码。",
|
||||
"This server is running version :running, but version :applied is installed": "此服务器运行的是 :running 版本,但已安装的是 :applied 版本",
|
||||
"To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.": "如需更新,请在安装目录中执行 sudo ./update.sh——它在下载或改动任何内容之前都会先询问。",
|
||||
"To update, run this in the install directory:": "如需更新,请在安装目录中执行:",
|
||||
"Version :running is installed, but the update has not been run": ":running 版本已安装,但尚未运行更新",
|
||||
"Updated ProjectSend to :to, from :from": "将 ProjectSend 从 :from 更新到 :to",
|
||||
"ProjectSend was updated to a new version": "ProjectSend 已更新到新版本",
|
||||
"Come and say hello": "来打个招呼吧",
|
||||
"Continue to the dashboard": "前往仪表板",
|
||||
"Discord": "Discord",
|
||||
"Join the Discord": "加入 Discord",
|
||||
"ProjectSend :version": "ProjectSend :version",
|
||||
"ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.": "ProjectSend 有一个 Discord:版本动态、遇到异常时的帮助,以及其他运行同款软件的用户。我们也在其中。",
|
||||
"What's new": "新增内容",
|
||||
"What each release brought to this installation.": "每个版本为这个安装带来了什么。",
|
||||
"Add the rest of your team": "添加团队其他成员",
|
||||
"Add your first client": "添加第一位客户",
|
||||
"Check the scheduler is running": "检查计划任务是否在运行",
|
||||
"Choose how your email looks": "选择邮件的外观",
|
||||
"Choose how your file lists look": "选择文件列表的外观",
|
||||
"Drop one in and choose who gets it.": "放一个进来,然后选择谁能收到。",
|
||||
"Essential": "必备",
|
||||
"Expiring files and queued email need it.": "到期文件和排队邮件都依赖它。",
|
||||
"Four layouts for the client-facing pages.": "面向客户页面的四种布局。",
|
||||
"Getting started": "快速上手",
|
||||
"Group your clients": "把客户分组",
|
||||
"Made with care in Argentina. Shared with the world.": "在阿根廷用心打造,与世界共享。",
|
||||
"Previewed on a real message.": "在真实邮件上预览。",
|
||||
"Resets and share links go out through it.": "密码重置和分享链接都从这里发出。",
|
||||
"Set up your mail server": "设置你的邮件服务器",
|
||||
"Share once instead of six times.": "分享一次,而不是六次。",
|
||||
"Staff accounts, each scoped by its role.": "团队账号,各自受角色限制。",
|
||||
"Thank you for installing ProjectSend": "感谢你安装 ProjectSend",
|
||||
"Thank you for updating ProjectSend": "感谢你更新 ProjectSend",
|
||||
"The people you send files to.": "你发送文件的对象。",
|
||||
"The short version of what to do on a new installation. Nothing here expires.": "新安装该做什么的精简版。这些都不会过期。",
|
||||
"You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "你现在使用的是 :version,一切都已恢复正常。感谢你继续把文件分享交给 ProjectSend。",
|
||||
"You are now on :version, up from :previous, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.": "你现在使用的是 :version,从 :previous 升级而来,一切都已恢复正常。感谢你继续把文件分享交给 ProjectSend。",
|
||||
"You are running version :version, and it is yours to keep. Here is what is worth doing first.": "你正在使用版本 :version,它属于你。以下是值得先做的事。",
|
||||
"Thank you for choosing ProjectSend": "感谢你选择 ProjectSend",
|
||||
"Your site is ready, and keeping it running is our job. Here is what is worth doing first.": "你的站点已就绪,运行维护交给我们。以下是值得先做的事。",
|
||||
"0 keeps everything. Unread notifications are never deleted, however old they are.": "0 表示全部保留。未读通知无论多久都不会被删除。",
|
||||
":version is available": ":version 可用",
|
||||
":version on :date": ":version(:date)",
|
||||
"Check now": "立即检查",
|
||||
"Checked a moment ago, so this is the answer from then.": "刚刚检查过,这是当时的结果。",
|
||||
"Checking…": "正在检查…",
|
||||
"Could not reach the update feed.": "无法连接到更新源。",
|
||||
"Dump the database before touching anything, and stop if the dump fails.": "在改动任何东西之前先导出数据库,导出失败就停止。",
|
||||
"Housekeeping": "日常清理",
|
||||
"Keep failed jobs for": "失败任务保留",
|
||||
"Keep read notifications for": "已读通知保留",
|
||||
"Last checked :date": "上次检查::date",
|
||||
"Leave PHP and the worker alone, for when you restart them yourself.": "不要动 PHP 和 worker,适合你自己重启它们的情况。",
|
||||
"Other options": "其他选项",
|
||||
"Report which version is installed and which is available, and change nothing.": "报告已安装的版本和可用的版本,不做任何更改。",
|
||||
"Retention updated.": "保留期限已更新。",
|
||||
"Run the script with no options and it asks about each of these as it goes. These are for the runs where you already know the answer.": "不带选项运行时,脚本会在过程中逐一询问这些内容。这些选项适用于你已经知道答案的运行。",
|
||||
"Skip the question about backups, for a run you are not sitting in front of.": "跳过关于备份的询问,适合你不在旁边看着的运行。",
|
||||
"The latest release (:tag) is not a version this can compare.": "最新发布版本(:tag)不是这里可以比较的版本。",
|
||||
"Two things pile up on their own. These are the windows the daily purges above work to.": "有两样东西会自行堆积。上面的每日清理就按这些期限执行。",
|
||||
"Up to date": "已是最新",
|
||||
"Update checks are not available on this edition.": "此版本不支持更新检查。",
|
||||
"Update options": "更新选项",
|
||||
"Updated": "更新于",
|
||||
"Version :version is available. You are running :current.": "版本 :version 可用。你当前使用的是 :current。",
|
||||
"You are up to date, running version :current.": "你已是最新版本 :current。",
|
||||
"days": "天",
|
||||
"sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.": "sudo ./update.sh --help 会列出所有选项,UPDATE.md 则说明整个流程,包括手动操作。",
|
||||
"Fetch dashboard news": "获取仪表板动态",
|
||||
"Purge API request logs": "清理 API 请求日志",
|
||||
"Purge erased accounts": "清理已抹除的账户",
|
||||
"Purge expired files": "清理过期文件",
|
||||
"Purge failed jobs": "清理失败的任务",
|
||||
"Purge orphan files": "清理孤立文件",
|
||||
"Purge read notifications": "清理已读通知",
|
||||
"Purge stale chunked uploads": "清理中断的分块上传",
|
||||
"Purge zip downloads": "清理 ZIP 下载",
|
||||
":action (:count)": ":action(:count)",
|
||||
":count matching this filter, newest first": "符合此筛选条件::count 条,最新在前",
|
||||
"A file was previewed via the public group listing": "文件通过公开群组列表被预览",
|
||||
"Anyone with the link can open an image, video, audio file or PDF in the browser instead of downloading it. Turn it off to make downloading the only way to see a public file.": "拿到链接的任何人都可以在浏览器中直接打开图片、视频、音频或 PDF,而不必先下载。关闭后,下载将是查看公开文件的唯一方式。",
|
||||
"Clients can preview files": "客户可以预览文件",
|
||||
"Downloads & previews": "下载与预览",
|
||||
"Downloads: :count": "下载::count",
|
||||
"Let visitors preview public files": "允许访客预览公开文件",
|
||||
"Lets clients open an image, video, audio file or PDF in the portal instead of only downloading it. Turn it off to make downloading the only way to see a file. Staff can always preview.": "让客户可以在门户中直接打开图片、视频、音频或 PDF,而不必只能下载。关闭后,下载将是查看文件的唯一方式。团队成员始终可以预览。",
|
||||
"No downloads match these filters.": "没有符合这些筛选条件的下载记录。",
|
||||
"Nobody has downloaded or previewed this file yet.": "还没有人下载或预览过这个文件。",
|
||||
"Preview :name": "预览 :name",
|
||||
"Previewed the file \":subject\" via the public group listing": "通过公开群组列表预览了文件“:subject”",
|
||||
"Previews: :count": "预览::count",
|
||||
"This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.": "本安装会自行构建镜像,因此拉取镜像不会有任何变化。如果这个版本改动了 composer.lock 或前端,请同时执行 composer install 和 npm run build——完整步骤见 UPDATE.md。",
|
||||
"To update, run this in the directory you cloned:": "如需更新,请在克隆得到的目录中执行:",
|
||||
"View all previews (:count)": "查看全部预览记录(:count)",
|
||||
"Your browser cannot play this file. Download it to view it.": "你的浏览器无法播放这个文件。请下载后查看。"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user