64 Commits

Author SHA1 Message Date
ignacionelson e9dabc39e3 Release 2.1.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:42:37 -03:00
Ignacio Nelson 9dcfe1eb26 Merge pull request #1634 from projectsend/docs/install-path-by-audience
Send users to the image and contributors to CONTRIBUTING
2026-08-18 18:02:18 -03:00
Ignacio Nelson c210187dab Merge pull request #1655 from projectsend/docs/security-policy
Say how to report a vulnerability
2026-08-18 17:46:57 -03:00
Ignacio Nelson 23aacc2b2a Merge pull request #1654 from projectsend/docs/dockerhub-overview
Track the Docker Hub description, and send migrators to the guide
2026-08-18 17:46:53 -03:00
ignacionelson 469ba8d8c8 Say how to report a vulnerability
There was no security policy in this repository, and no "Report a
vulnerability" button either — private reporting was switched off, so the
Security tab offered an outside researcher nothing to press. The Docker Hub
description meanwhile told people to report privately through GitHub
advisories, which is advice for a thing that did not exist. The likely
outcome of that combination is somebody filing a working exploit as a
public issue, having tried to do the right thing first.

Reporting is on now, for this repository and for both public packages. This
is the policy that goes with it.

v1's policy was the starting point and about half of it survived. The
address is the same, contact@projectsend.org, which is still live and still
in CONTRIBUTING.md and both CLAs. "Do not open a public issue" is still the
point. What did not carry: it predates private reporting and would have
contradicted what we already tell Docker users, and it sent readers to v1's
SECURITY_HARDENING.md — eight kilobytes about hand-writing Nginx and Apache
rules to keep uploads private, which in v2 ship in the nginx config and are
already in place inside the image. Pointing a v2 operator at it would be
pointing them at a different architecture.

What is new is what v1 never needed: which versions are supported, now that
there are version numbers and two packages on their own lines, and a
sentence on what is not a vulnerability — an installation that skipped the
install guide, an unread scanner report, a dependency finding Dependabot
already watches.

The response times are deliberately modest and honest about the size of
this project, including what to do about silence. Better that than an SLA
nobody is on call for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:35:18 -03:00
ignacionelson be44b4b6aa Track the Docker Hub description, and send migrators to the guide
This file is what Docker Hub shows above the pull command, and it has been
sitting untracked since it was written — nothing to review it against,
nothing to stop it drifting from the compose file and the entrypoint it
describes, and nothing in the repository if the machine holding it died.

Verified against what it claims rather than read for tone. The compose
block has the same services, images and environment variables as
compose.example.yaml, with nothing extra in either. supervisord really does
run nginx, php-fpm, the queue worker and the scheduler. APP_KEY really is
generated once and kept on the storage volume with .env symlinked to it.
The container really does wait for MySQL and then run
`php artisan projectsend:update`. `projectsend:admin --if-none` is what
makes "ignored once any user exists" true. nginx really does serve
protected downloads with X-Accel-Redirect, which is the reason the image
carries a web server at all. MySQL 8.0-or-newer matches INSTALL.md word for
word, and every external link resolves.

The one thing that was wrong for its audience: the Legacy section pointed
at the migration tool's repository, and everybody reading this page is on
Docker — where the tool cannot be installed the way its README implies,
because the image ships the application already built and carries no
Composer. It now points at the guide, and names the section written for
this image today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:26:42 -03:00
Ignacio Nelson 7c6c49e5f4 Merge pull request #1653 from projectsend/chore/translations-for-2-1-0
Translate the pre-2.1.0 strings into all sixteen locales
2026-08-18 16:59:54 -03:00
Ignacio Nelson 31f9b22b53 Merge pull request #1652 from projectsend/docs/docker-migration-path
Give the official Docker image a way to migrate
2026-08-18 16:59:46 -03:00
ignacionelson dd779fafbe Let the scheduled task names be translated
Ten task names sat in a private const as bare strings — 'Purge erased
accounts' and its nine siblings — so the Scheduler screen listed ten
English rows in the middle of an otherwise fully translated page. Found by
looking at the Spanish screen while checking the translation pass, not by
any check, because nothing could have reported it: the scan only sees
literals inside __(), and prose held as data under a key is invisible to
it. Nine of the ten had never been translatable in any language. The tenth,
'Check for updates', looked translated purely by coincidence — a button
elsewhere uses the same words, so the catalogue happened to have it.

A const cannot call __(), so the map becomes a method. That is the whole
change in substance.

The keys are untouched and stay untouched: they are the command names, they
are what the run history, the detail map, the frontend and the test
asserting this list matches the schedule all match on, and they are what
somebody types to run the thing by hand. Only the values were ever
language. The screen still prints the command name verbatim under each
translated label, which is the half a reader would copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:49:27 -03:00
ignacionelson 18917adaf2 Translate the pre-2.1.0 strings into all sixteen locales
The update-check work, the update-options dialog and the housekeeping
retention settings landed in English, as feature work here always does.
Twenty-eight strings, missing from every one of the sixteen catalogues,
which is a release gate rather than a nicety: build-release.sh refuses to
build while a locale is short.

They are the strings somebody reads while deciding whether to touch a
running installation — what --backup does, what happens if the dump fails,
that 0 means keep everything and that unread notifications are never
deleted whatever the setting says. Being English in a Japanese install is
worse for those than for most.

Placeholders survive verbatim, checked mechanically across all sixteen
rather than by eye: :version, :current, :date and :tag, and the literal
`sudo ./update.sh --help` and `UPDATE.md` inside the sentence that names
them.

"days" is the standalone suffix beside a number input, which is the Slavic
numeral trap — Polish, Czech and Russian inflect the noun according to the
number in front of it, and the number here is whatever the operator typed.
No single form is right for every value, so each takes the form that reads
correctly for most: dni, dní, дней.

Two collisions worth naming. "Up to date" and "Updated" are different
things — a status and a row label — and several locales would render both
the same word; they are deliberately distinct (aggiornato/ultimo
aggiornamento, up-to-date/bijgewerkt). Everything else follows the wording
already in each catalogue rather than inventing a second vocabulary for
the same ideas.

Additive only: 464 lines added, 16 removed, and every one of those 16 is a
trailing comma. Verified by scan (missing: 0 everywhere), by the Locale
suite, and by looking at the screen — the Spanish labels are the longest
of the sixteen and still fit their controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:53:47 -03:00
ignacionelson 8156664ccc Give the official Docker image a way to migrate
The guide had no answer for the install method we recommend most. The
published image carries no Composer — it ships the application already
built and has no use for one — so `composer require`, the first command in
Step 1, is not a thing a Docker reader can run at all. The guide's Docker
line assumed the stack in this repository, which builds from source and does
have one.

Composer is a PHP file, and PHP is what the image is. Fetching it into /tmp
for the length of the migration works, and the whole path is verified rather
than plausible: the tool installs, all four projectsend:migrate:* commands
register with no package:discover run by hand, and the export script the
Bundle route needs is where the guide now says it is.

Two details in there are load-bearing rather than decorative. `-u www-data`
because exec lands as root and a root-owned vendor/ is a problem the
application meets later rather than now. `--update-no-dev` because the
shipped lock file knows about pest and phpstan, and a production install
should not grow a test suite on the way past.

The section says plainly that the install lives in the container's writable
layer and does not survive the container being replaced. That is honest
about a real limit, and the reason it is acceptable is worth saying too: the
run's progress and its id map are in the database, so losing the package
costs an install, not a migration.

Step 1 is now three self-contained sections rather than one set of commands
with exceptions bolted on, because the previous shape is what let a reader
follow instructions that could not work on their install.

Verified against the real image, built locally from the release zip — the
Docker Hub repository is still private, so pulling it was not an option.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:32:45 -03:00
Ignacio Nelson ae510e3e34 Merge pull request #1651 from projectsend/docs/step-1-by-install-type
Say what Step 1 actually does on a zip install
2026-08-18 15:29:31 -03:00
Ignacio Nelson 352b19a061 Merge pull request #1650 from projectsend/fix/community-modules-versioned
Depend on a version of community-modules, not a branch
2026-08-18 15:11:41 -03:00
ignacionelson d1acf18d67 Say what Step 1 actually does on a zip install
Step 1 told everybody to run `npm run build`. On a release zip there is
nothing to run it with: the zip ships its assets already compiled and
deliberately without the toolchain that compiled them, so there is no
package.json. A customer migrating a Debian install got that far, found the
file missing, and asked whether to wait for a fix. There was nothing to wait
for — they were finished and could not tell.

The larger thing that step was hiding is that `/system/migrate` does not
exist on a zip install either. A package's screens enter the frontend bundle
when that bundle is built, and app.tsx globs them at build time — for a zip
that is when the release was built, necessarily before the package was
installed. So the guide led with a screen that reader can never open, and
called the commands an alternative.

For a zip install the commands are not an alternative, they are the
interface, and they do everything the screen does. Step 1 now splits by how
ProjectSend got onto the machine, says so plainly, and points a zip reader
straight at Step 2 rather than leaving them to work out that the missing
screen is expected.

Three later steps assumed the screen too. Choosing Direct, choosing Bundle,
and downloading the export script all now read for both, and the export
script is named at its path in vendor/ — which is where it lives on every
install, and where a reader without the screen would otherwise have to guess
it was.

Not addressed here: the official Docker image ships neither Composer nor
npm, and its application directory is not a volume, so the tool cannot be
installed there at all. That is a gap in the product rather than in the
guide, and it needs its own answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:11:15 -03:00
ignacionelson 2567d9f193 Depend on a version of community-modules, not a branch
projectsend/community-modules is published and tagged now, so the
dependency can name a version. It could not before: with nothing to point
at, composer.json carried "*" against a vcs entry, which meant every
install resolved the default branch and got whatever had been pushed to it.
`composer validate` has been saying so on every run — "unbound version
constraints (*) should be avoided" — and it is right, because a dependency
that tracks a branch cannot be reasoned about and cannot be rolled back.

So: "^1.0", and the vcs entry goes. It has to go rather than merely stop
being needed, for the same reason the migration tool's did — a repository
declared in composer.json is canonical and outranks packagist.org, so
leaving it would keep serving dev-main off the default branch and shadow
every release ever tagged. Silently, which is the part that matters.

The lock moves with it: dev-main 311883a becomes v1.0.0, sourced from
Packagist rather than a git clone of a moving branch. Same code — the tag
was cut from that commit — but now it is a code somebody can name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:00:49 -03:00
Ignacio Nelson 7093d1e24b Merge pull request #1649 from projectsend/fix/migration-tool-on-packagist
Let Packagist answer for the migration tool
2026-08-18 14:50:41 -03:00
ignacionelson d32562e75c Let Packagist answer for the migration tool
projectsend/v1-migration-tool is published now, and tagged, so
`composer require projectsend/v1-migration-tool` resolves on its own —
on any installation, new or existing, with nothing added to composer.json.
That is what the guide always claimed and what a customer's Debian install
proved it was not.

The vcs entry has to go, not just stop being necessary. Repositories
declared in composer.json are canonical and outrank packagist.org, so
leaving it would keep serving dev-main off the default branch and silently
shadow every release ever tagged. No error, no warning, just the wrong code
indefinitely — the failure mode you find months later.

So the manual fallback goes too, from the guide and from the teardown note
that told people to keep the entry. Neither describes anything that exists
any more, and instructions for a state the reader is not in are how the
last round of this went wrong: the note about a private repository outlived
the repository being private, and got skipped for looking inapplicable
while still being required.

The changelog entry is rewritten rather than added to. It described
shipping a pointer, which was the previous fix and lasted a day; what
landed is that the tool is published and versioned, and an installation can
finally say which version of it ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:44:17 -03:00
Ignacio Nelson 24e3019e21 Merge pull request #1647 from projectsend/fix/deleted-rows-hold-their-slug
Give a deleted folder's name back
2026-08-18 12:22:25 -03:00
ignacionelson 98597d462d Give a deleted folder's name back
Delete a folder called Test and you could never have a folder called Test
again. The deletion worked, the folder left the screen, and the name went
with it — permanently, with an error that named a collision against a row
the interface will not show you and offered nothing to do about it.

Files and groups had it too. All three carry a unique index on slug and
all three soft-delete, so the trashed row sat in the index holding a name
nothing could reach. A public one failed outright at the validator, which
checks the table and therefore sees rows the screen does not. A private
one failed more quietly: the derived slug stepped around the trashed row
into report-2, then report-3, once per deletion, climbing forever.

The reservation was deliberate — a trashed row's slug was kept so that
restoring it could not land on somebody else's URL. But nothing in this
application restores anything. There is no restore() call, no route, no
screen; File's own comment says as much. Soft deletes are here so rows can
outlive their delete for foreign keys, the activity log and the erasure
grace period, never so they can come back. The slug was being held for a
page that could not return, and route binding already 404s the trashed row
in the meantime.

So deleting now hands the slug back, and the database is what makes that a
rewrite rather than a gentler lookup: teaching the collision checks to skip
trashed rows would leave two rows holding "report", which the unique index
rejects whatever the application thinks. The slug moves to report__deleted-42
instead. 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 land on a vacated one. That is a guarantee about
the character class rather than a hope about collisions.

The format lives in VacatedSlug rather than on the trait because the
migration needs it too and a trait constant cannot be reached through the
trait's own name — the first version of this was a fatal error waiting for
whoever ran migrations. The migration matters as much as the hook: without
it the fix only helps installations that have never deleted anything, and
every name already buried stays buried.

The collision checks still count trashed rows. It costs nothing and keeps
them honest about what the index will accept if a row is ever soft-deleted
by something that bypasses model events.

previous_file_id had this same bug and was fixed this same way, in
File::detachOnDelete — a trashed row holding its predecessor's unique slot
so the chain could never be re-linked. This is that fix, for the other four
unique indexes' worth of the same mistake. users.email is the one left, and
is deliberately not in here: an email address is a login identity rather
than a URL handle, and freeing it silently is the wrong answer.

Fixes #1645

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:11:02 -03:00
Ignacio Nelson 6a4e6df21a Merge pull request #1646 from projectsend/fix/migration-tool-composer-discovery
Tell Composer where the migration tool lives
2026-08-18 11:46:40 -03:00
ignacionelson bba0d78499 Tell Composer where the migration tool lives
`composer require projectsend/v1-migration-tool` — the first command in the
migration guide, and the first thing anybody moving from Legacy types —
fails on a fresh installation with "Could not find a matching version of
package". The tool is not on Packagist, and nothing in composer.json said
where else to look, so Composer had no way to find it and said so.

The manifest already had the answer four lines up: community-modules, also
unpublished, resolves through a vcs entry. The migration tool now has the
same one, so the documented command works as documented.

The guide said to add that entry by hand, but behind "while the repository
is private" — which stopped being true when the repository went public,
and a note that reads as inapplicable gets skipped. The step it described
was still mandatory, because public and published are different things.
That paragraph is now a fallback for installations predating this commit,
which do still need it, keyed on the error text rather than on a condition
the reader cannot check.

The teardown step is inverted to match: it used to suggest removing the
repositories entry after the migration, which would now break a second
attempt. It says to leave it.

Changing repositories invalidates composer.lock's content-hash, hence the
one-line lock update.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:24:52 -03:00
Ignacio Nelson b33f4c389a Merge pull request #1643 from projectsend/feat/pre-2.1.0-polish
Finish the update story before 2.1.0
2026-08-17 22:00:05 -03:00
ignacionelson cab9291d29 Stop two tables from growing forever on an untended installation
Failed queue jobs and read notifications both grow with use, and neither
ever shrank on its own. The failed-jobs list waited for somebody to press
"Delete all failed" — a fine tool for a backlog you are looking at, and
the only thing that ever emptied it. Notifications had nothing at all: one
row per recipient per event, kept for the life of the installation, on
what is easily the fastest-growing table here.

Both now have a retention window, set together on the Scheduler screen
under Housekeeping, and a nightly purge that honours it. Thirty days for
failed jobs and ninety for read notifications, and zero means keep
everything — the explicit choice somebody makes when a failure is evidence
rather than debris.

Unread notifications are never deleted, whatever their age. A notification
nobody has looked at is the one row in that table still doing its job, and
somebody back from four months away should find their news rather than a
clean slate. The activity log is untouched by any of this: it is an audit
trail, and it is never pruned.

Two things came out of building it. The API request log purge has been
running nightly since it shipped without ever appearing on the Scheduler
screen — so a failure of it was invisible on the screen that exists to
make failures visible — and there is now a test asserting the screen's
list and the schedule are the same list, because they had already drifted
once and would again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:44:06 -03:00
ignacionelson b671d0d74a Put the update script's useful options one click away
The screen that tells somebody how to update names one command and stops,
which is right — the command is the procedure. But two of the options
behind it are the ones an administrator wants at exactly that moment:
whether it can take the backup for them, and whether they can just look
without changing anything. Both were documented only in UPDATE.md and in
--help, neither of which is open on the screen they are reading.

They are behind an "Other options" link rather than printed, so the page
in its resting state is unchanged and the one command stays the thing you
see. Four lines, the two above plus the two for a run nobody is sitting in
front of.

It opens from the dashboard card and from the update dialog both, which
means a dialog on top of a dialog in the second case. That is the right
shape here: the reader asked for a footnote to what they are already
reading, and Escape puts them back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:37:01 -03:00
ignacionelson 44f015c066 Let the one essential step nobody could finish finish itself
"Check the scheduler is running" was marked essential and hardcoded
unticked, so the getting-started list could never be completed — the two
steps that tick themselves sat above one that never would, which reads as
a checklist that has quietly stopped working.

It is answerable, and the screen it links to was already answering it: a
scheduled-run row exists once the scheduler has run on this server at all.
That is precisely what the step asks. A run that failed counts, because a
failure still proves cron reaches this installation; why it failed is the
Scheduler screen's job and the step links there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:36:07 -03:00
ignacionelson e87ceb60ba Say what the update check found, not just that it ran
The Scheduler screen printed "Check for updates · Succeeded · —" and left
it there. What it found — the whole reason that job exists — was in the
settings, which that screen never read. Somebody opening it to ask "is
there a new version?" got the answer to "did the job run?"

The Message column now carries "Up to date" or the version that is
waiting. A failure's own message still wins: what the last successful run
found is not the answer to why this one broke.

Joined at render time rather than recorded by the command, because
Laravel's scheduler fires its finished event after the command returns and
overwrites whatever the command wrote — which is exactly why that column
was empty in the first place. Reading the settings instead also keeps the
line true when the new Check now button did the work rather than the
nightly run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:36:01 -03:00
ignacionelson d888145b21 Say when this installation was last updated
The update command has been recording the version it applied and the
moment it did since it shipped, and exactly one thing read it: the notice
that appears when the running code and the applied version disagree. So
the fact was written down and then only ever mentioned when something was
wrong.

About now answers the ordinary version of that question — "Updated to
2.1.0 on 17 Aug 2026" — beside the version it already showed. It is the
answer to "when did this change?", asked after something looks different,
and by whoever inherited a server from the person who set it up.

Absent rather than approximated on an installation that has never been
updated through the command: a fresh install has no update to date, and
"unknown" is noise. Same gate as the rest of that block, so a managed
installation — where the version is not the reader's concern — is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:36:01 -03:00
ignacionelson 997debc6a3 Let somebody ask for an update instead of waiting for tonight
The check ran daily and there was no other way to run it. An administrator
who has just read that a release fixes the thing bothering them had to
reach a terminal — or wait until tomorrow to be told what the project
announced this morning.

There is now a Check now button beside the setting that schedules it. It
says what came back: the version waiting, or that this installation is
already on the newest. The time of the last check sits next to it, because
the notice on the dashboard is only as good as when it was last refreshed
and nothing said when that was.

Deliberately not gated on the daily-check setting. Switching that off says
"do not have my server phone out unattended", which is not the same
sentence as "refuse to answer when I ask" — so the button works either way
and the setting keeps governing only the schedule.

The work moved out of the command into CheckForUpdates, because the part
that must not drift between the two callers is the part with consequences:
which staff get notified, and the guard that stops them being notified
again for a release they already know about. A second copy of that in a
controller would have been found wrong six months later by somebody
receiving the same notification every time a colleague pressed a button.

Two throttles, and the second is not redundant. The route's bucket is per
user; GitHub's limit is per server address, so two administrators each
within their own allowance can still exhaust the installation's. The
cooldown is installation-wide and costs no new setting — it reads the
timestamp every check already writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:35:48 -03:00
ignacionelson 9192779ee4 Close four small gaps before the release goes out
Four unrelated one-liners, each already written down and none of them
worth a branch of its own.

The lock was still pinned to the community package's previous commit,
which is the one before it started shipping its own sixteen catalogues.
The mechanism that carries a package's translations to the browser landed
here last week; without this bump the release would have shipped that
mechanism with nothing to carry, and the Custom Assets screen would have
stayed half-English in every language.

The stock `local` disk had `serve` left on. Nothing in this application
writes to it, so the framework's /storage route was a door with nothing
behind it — but it was still a door, and closing it costs one word.

nginx evaluated `\.php$` before `/protected-files/`, so a protected path
ending in .php would have reached the PHP handler instead of streaming
under the sandbox headers that block sets. Not reachable on a default
install — the upload allowlist refuses php and X-Accel paths are UUIDs —
but the guarantee read stronger than it was. `^~` makes it true.

And `.release-build` is now ignored by eslint, so linting after building a
zip stops walking the vendored minified JS inside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:28:16 -03:00
Ignacio Nelson a8bc2156cd Merge pull request #1642 from projectsend/feat/package-translation-paths
Let a package's translations reach the screen it wrote
2026-08-16 15:27:56 -03:00
ignacionelson 928173e8be Let a package's translations reach the screen it wrote
The frontend's catalogue was read straight out of lang/{locale}.json, so
it held exactly the strings this repository owns. That was true for as
long as this repository owned every screen — but the companion packages
own several: Branding, Custom Assets, the whole v1 import. Their strings
have been rendering in English in all sixteen languages, in silence,
because a package catalogue registered through loadJsonTranslationsFrom()
never got as far as the browser.

Asked of the framework's own loader now, which is where that registration
already lands. Same answer as before for this installation — no package
registers a path today, and the merged result is byte-identical to the
file — and the right answer the moment one does.

Precedence comes free and is the useful way round: the loader merges the
application's own catalogue last, so an installation can override a
package's wording without editing the package. There is a test for that,
because it is the kind of ordering that gets reversed by accident.

One thing the test needed and is worth knowing: SetLocale honours an
account's chosen language only while that language is enabled for the
installation, and the Settings cache outlives RefreshDatabase. A test that
sets users.locale and assumes it takes effect gets English and a very
confusing failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 14:44:58 -03:00
Ignacio Nelson c052175690 Merge pull request #1641 from projectsend/chore/prune-stale-translations
Clear out the 29 catalogue entries that stopped applying
2026-08-16 14:40:51 -03:00
ignacionelson de615cbd79 Clear out the 29 catalogue entries that stopped applying
Nothing was missing this pass — the new strings were translated as they
landed — so this is step 3, the orphan review that has been deferred for
weeks with the count sitting at 277.

Of those 277, **188 are laravel-lang's own JSON keys** (HTTP status names,
"Whoops!", "Verify Email Address", the pagination labels). Upstream and
human-maintained; the skill says to leave them alone and a lang:update
would put them straight back. Identified by diffing against
vendor/laravel-lang/*/locales/es/*.json rather than by eye.

Another **60 are strings the scanner cannot see**: prose held as data and
rendered through t(VAR) — the dashboard widget labels, the portal's sort
options, EmailTemplateSlot's descriptions, CommentingRules' refusals,
convert.tsx's plural pairs — plus three that belong to the private
packages, which the scanner does not read at all.

That leaves **29 that are genuinely ours and genuinely gone**, and this
removes them. Each was checked as a quoted literal across app/, routes/,
database/, resources/ and all three package repos, then re-checked against
every data-driven translation source. The three that looked alive
("Assigned", "Result", "System settings") turned out to be substrings of
longer live strings — "Assigned the file :subject to :target", the
LdapProbeResult class name, "System settings were updated".

Two were reworded rather than removed, which is the case that matters:
the comment-visibility description and the public-listing paragraph both
say something slightly different in the code today, so their old
translations had silently stopped applying in all sixteen languages while
the new ones are already in place.

Orphans: 277 -> 248, and every one of the 248 is now accounted for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:31:00 -03:00
Ignacio Nelson 3f36630d46 Merge pull request #1640 from projectsend/copy/cloud-getting-started-hero
Say the right things to a hosted customer
2026-08-16 01:00:14 -03:00
ignacionelson a8a7f3f340 Send each edition to its own front door, and stop asking payers for money
Two things on the About screen were written when there was only one
edition. "Website" pointed at projectsend.org for everybody, and the
donation link was offered to hosted customers who are already paying for
this — on the same screen that thanks them for choosing it.

projectsend.org is the way in for the software you run yourself and
projectsend.cloud is the way in for the hosted service, so `links.website`
now resolves to whichever one the reader is actually using. That reaches
further than About by design: the "Powered by ProjectSend" line at the
foot of every outgoing email and on every client-facing page is where a
recipient meets this product for the first time, and sending a hosted
customer's recipients to self-hosting instructions is the wrong door.

The donation link is *omitted* rather than hidden by the page, so a
surface added later cannot ask a paying customer for money by forgetting
to check. Its TypeScript type is optional now, which makes the compiler
enforce the same thing.

Also fixed on the way past: the settings footer hardcoded the text
"projectsend.org" next to that link, so on the hosted service it named a
site it did not link to. It reads the host off the resolved URL now.

Verified in a browser against both editions, not only in tests. Cloud:
projectsend.cloud, no donation link, on both screens. Community:
projectsend.org and Open Collective, exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:55:41 -03:00
ignacionelson 29f1eaaa1b Do not thank a hosted customer for installing anything
Nobody on a managed installation installed ProjectSend, and the version
they are running is not theirs to keep or to change — it is looked after
for them. "Thank you for installing ProjectSend. You are running version
2.0.0, and it is yours to keep" thanked them for somebody else's work, on
the one screen whose entire job is to sound like a person wrote it.

Cloud now reads "Thank you for choosing ProjectSend" over "Your site is
ready, and keeping it running is our job." The version comes out of the
sentence there: it is true, and it is none of their concern, which is
exactly what makes it noise.

Community is unchanged, and so is the wording somebody sees revisiting
the page later — that one is neutral already.

Branched on the shared `edition` prop rather than a capability, because
the question being asked really is which edition this is: SystemUpdates
happens to line up today, but it is about who may update, not about who
installed.

Both new strings are in all sixteen catalogues in the same commit, since
this is a two-string change and splitting it would leave the hosted
greeting English-only for however long the next pass takes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:43:23 -03:00
Ignacio Nelson 57c4b892ce Merge pull request #1639 from projectsend/feat/getting-started
Show a new installation's administrator around, once
2026-08-15 20:32:55 -03:00
ignacionelson 090e975bb3 Translate the two greeting pages into all sixteen locales
Twenty-four strings each: the thank-yous, the eight quick-start steps and
their one-line descriptions, the Essential label, and the line from the
website at the foot of both pages. 336 entries in, 48 out.

The three removed are the what's-new hero sentences reworded earlier
today — a translation that silently stopped applying the moment the
English changed, which is exactly the orphan the skill says to clear. All
three were checked against resources/ and app/ first; none survives as a
literal anywhere.

Register follows each catalogue rather than the language's default, and
terminology follows the screen each step links to: the scheduler step
says "programador de tareas" in Spanish because that is what the sidebar
calls it, "Planer" in German, "Harmonogram" in Polish.

"Made with care in Argentina" is translated rather than left in English —
it is a sentence about the people who made this, and the people reading
it in Japanese deserve to read it too.

Checked in the browser with the interface in Spanish: "Gracias por
instalar ProjectSend", "Estás usando la versión 2.0.0, y es tuya",
IMPRESCINDIBLE on the mail server and the scheduler, and "Hecho con
cariño en Argentina. Compartido con el mundo." under the fold line. Scan
reports 0 missing in all sixteen; the Locale tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 20:29:09 -03:00
ignacionelson b72d30d89e Sign the two greeting pages the way the website signs itself
"Made with care in Argentina. Shared with the world." — projectsend.org's
own line, at the foot of the two screens where somebody is being greeted
rather than getting on with their work.

Drawn rather than set as the flag emoji the website uses. Regional
indicator pairs have no glyphs on Windows or on most Linux desktops, and
both fall back to a pair of small letters — the first render of this
component read "AR Made with care in Argentina" on the machine that
screenshotted it, which is what a good half of the people self-hosting
this would have seen. Eleven lines of SVG look the same everywhere.

The Sun of May is a plain disc: at sixteen pixels its rays are a smudge,
and a smudge reads as a rendering fault rather than as a flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 20:17:17 -03:00
ignacionelson 046567fdfc Say thank you, and make the list scannable at a glance
Both greeting pages read like status reports. The install page opened
":name is installed and yours" without saying which version, the update
page opened "The update finished, and everything came back up", and the
quick-start list was eight full-width rows of two-line descriptions —
about 1400px, with the last two steps and the invitation below the fold
on a laptop.

The install page now thanks somebody for installing ProjectSend and names
the version they are on. The update page thanks them for updating and for
continuing to trust it with their file sharing. Both revert to plain
wording when the page is opened later from a link: thanking a reader
again for something they did months ago is the cold thing, not the warm
one.

The list becomes a two-column grid of icon cards — four rows instead of
eight, 1000px against 1490px, which is one screen. Icons come from the
sidebar's own vocabulary, so the chip on a card is the icon on the screen
it opens. Descriptions are one short clause each; the screen at the other
end explains itself.

And the steps stop pretending to be equally urgent. QuickStart now says
which are essential — the two that make this application do anything at
all, the mail server, the scheduler — and those carry an amber chip and
a label, against the brand colour for everything else and green for the
done ones. Amber is not invented here: it is the warning Alert variant's
palette, reused verbatim so dark mode is somebody else's solved problem.

The Discord card was two identical copies within an hour of each other,
so it is one component now, before the pair could drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 20:05:33 -03:00
ignacionelson 4ce6793da9 Show a new installation's administrator around, once
Setup ended by handing somebody a login form and an empty dashboard.
Everything this application can do was one menu away, and which menu was
theirs to discover.

The first time the administrator signs in to a new installation they now
land on a short ordered list of what is worth doing first — add a client,
upload a file, group the people who get the same things, choose how the
file lists and the email look, point it at a mail server, add the team,
check the scheduler — each a link straight to the screen that does it.

The list is filtered twice, and both filters matter. By permission,
because a link that answers 403 is worse than no link. And by edition:
a managed installation is not sent off to configure a mail server
somebody else runs, to create staff accounts that are not its to create,
or to check a scheduler it does not host. Those three drop out on Cloud
and the other five remain.

Two steps tick themselves, because the database can answer them: a client
exists, a file exists. Nothing else is checkable without guessing — a
theme that was never changed looks exactly like one chosen deliberately —
and a tick meaning "we assume so" is worse than no tick.

The invitation to the Discord is at the very bottom, after the list.
Somebody who has just installed this came with a job in mind, and opening
with a social invitation is the fastest way to lose them.

The marker is raised where a first administrator comes into existence —
the setup screen and `projectsend:admin`, so a container provisioned from
environment variables is welcomed too — and it is false by default, so an
installation that updates into this feature is not congratulated on an
install it finished a year ago.

RedirectToWhatsNew becomes RedirectToGreeting and answers for both: they
are the same interruption, and a second middleware on the same route
would have to know about the first to avoid arguing with it. Installing
wins; release notes for a version you never ran are the wrong greeting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:44:22 -03:00
Ignacio Nelson abe97a1f4d Merge pull request #1638 from projectsend/chore/translate-whats-new
Translate the post-update welcome into all sixteen locales
2026-08-15 15:49:30 -03:00
ignacionelson 55b02d510d Translate the post-update welcome into all sixteen locales
The eleven strings the welcome page introduced, now in every catalogue:
176 entries across sixteen files, appended rather than merged in, so the
diff is only what was added and no existing line moves.

Two of them are deliberately identical to the English. "Discord" is a
product name, and "ProjectSend :version" is a name and a placeholder with
nothing between them to translate — the scan counts both as untranslated
and that is the right answer.

Register follows each catalogue rather than the language's default: Sie
in German, vous in French, je in Dutch, Ty in Polish, tú in Spanish. The
thank-you sentence was written to sound like somebody meant it, since a
literal translation of a warm line is a worse translation than a free one.

Checked in the browser with the interface in Spanish, not only scanned:
"ProjectSend ahora está en la versión 2.0.0", "Ven a saludarnos", "Únete
a Discord", "Novedades" — every line fitting its space. Scan reports 0
missing in all sixteen; the Locale tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:44:18 -03:00
Ignacio Nelson 120ba10972 Merge pull request #1637 from projectsend/feat/post-update-welcome
Greet the administrator once, on the first visit after an update
2026-08-15 14:30:09 -03:00
ignacionelson e3c58818e2 Give the invitation the brand colour, so it reads as the feature
On `muted` it was a grey box between two things people actually look at.
It now sits on `accent` — the brand colour at surface strength, which
already carries a dark-mode counterpart, so this is legible in both
without a hardcoded purple anywhere and follows the palette on an
installation whose branding replaces ours.

Its button is filled and the one below it is not. Outline was wrong here
twice over: its hover state is this exact background, so the button
vanished under the cursor, and "continue to the dashboard" is leaving —
not the thing this page is encouraging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:25:36 -03:00
ignacionelson 6ddfc1aa5d Greet the administrator once, on the first visit after an update
An update finished and nothing said so. The dashboard looked identical to
yesterday's, and whatever the release brought was in a file nobody opens.

The first time the installation's administrator opens ProjectSend after
an update, they now land on a page that names the version they are on,
invites them to the Discord — the same invitation update.sh prints, made
again where they are actually looking — and then lays out what the
release brought.

The notes come from CHANGELOG.md inside the release, not from GitHub: the
one moment this page exists for is the moment after an update, possibly
on a server with no outbound access, describing code already on disk.
Parsed rather than rendered, so nothing in it can become HTML.

Once, and to one person. The update happened to the installation, so
greeting five staff members — each having to dismiss a page they did not
ask for — would turn a pleasant moment into a support question. It goes
to the oldest active administrator, which on any installation that went
through setup is whoever set it up. No owner flag was invented for this:
administrators are equal in authority, and changing that for a greeting
is not a trade worth making.

Only forwards, and only for a real update. A fresh install has nothing to
catch up on, a container reboot has not updated anything, and somebody
restoring an older release is dealing with a problem rather than
celebrating. Managed installations never see it at all — nobody signed in
there performed the update it thanks them for, which is the same gate the
System card and About's environment block already carry.

The redirect is attached to the dashboard alone, not the web group: it
catches a login and the sidebar logo both, without ever interrupting a
download to congratulate somebody. Reading the page clears the marker,
but the address keeps working — closing it by accident should not be
unrecoverable — and About now links to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:22:17 -03:00
Ignacio Nelson 463f2aac30 Merge pull request #1636 from projectsend/feat/update-discord-invite
Invite people to Discord when the update finishes
2026-08-15 12:56:26 -03:00
ignacionelson 982a682803 Invite people to Discord when the update finishes
The last thing an operator sees after a successful update is now where the
people are. Printed rather than asked: the answer to "would you like to
join?" is a browser, and this runs over SSH on a server that has none, so
a y/n could only print the URL its "no" branch prints anyway — and --yes
would answer it on behalf of a cron job that cannot join anything.

Only on success. --check is a report somebody may be scripting, and a
failed run's last line should stay the recovery instructions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:50:19 -03:00
Ignacio Nelson f446398dfd Say which step is missing instead of failing blankly (#1633)
Somebody followed the README's Docker quickstart, which starts the
development stack, and got three failures in a row with nothing to search
for (#1627): the worker died once a second on a missing autoloader, the
site answered a bare 500, and once dependencies were installed by hand the
setup screen threw ViteManifestNotFoundException.

None of that is wrong behaviour for a clone — vendor/ and public/build/
are deliberately not in git — but every one of those failures kept its
cause to itself.

The preflight guard exists to turn "this was never set up" into a
sentence, and it runs before the autoloader precisely so it can. It now
answers two more questions: dependencies not installed, and frontend not
built. The dependency check goes first, before the .env one, because the
fix that branch prints — php artisan key:generate — cannot itself run
without the autoloader, so reporting the key first hands somebody a second
and more confusing error. A running vite dev server counts as built:
public/hot means the assets come from there, and blocking a developer
mid-session would be worse than the exception this replaces.

The worker and scheduler exec straight into artisan, so before composer
install they died instantly and restarted forever, filling the log that
had to be read to fix it. They now print what is missing and exit slowly,
and recover on their own once it is there. The scheduler gains the restart
policy the worker already had — without one it exits during that window
and stays exited, and scheduled work then silently never happens.

Rehearsed on a genuine clone of the public repository, following the
reporter's exact path: worker prints instructions instead of fatals (2
restarts in 30s, not 30), the browser gets "ProjectSend is not installed
yet" naming composer install, then "not configured yet", then "not built
yet" naming npm run build, then the setup screen.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 02:21:29 -03:00
ignacionelson 745f24c7d9 Send users to the image and contributors to CONTRIBUTING
The README told everybody to clone the repository and run the development
stack, which builds nothing and installs nothing — so a user following it
got a crash-looping worker and a 500, and reported it as a bug (#1627).
They were reading the right document; it was pointing at the wrong stack.

Getting started now says what each path is for. Users fetch
compose.example.yaml and run the published image, which ships vendored and
pre-built and needs no toolchain — it has existed all along and was linked
from nowhere in the README. Contributors get one paragraph saying a clone
is a development copy, not an installation, and a pointer to
CONTRIBUTING.md, whose sequence was already correct.

CONTRIBUTING gains the sentence its readers were missing — why those steps
exist at all — plus the two things a contributor now meets: containers
that name the missing step instead of dying silently, and the fact that a
second checkout shares one Compose project name and will take over the
first one's containers. That last one cost me this working copy's stack
for a few minutes.

Also: DOCKER.md and INSTALL.md both linked README.md#development, an
anchor that has never existed. And UPDATE.md's build-from-source path told
operators to pull and rebuild with no dependency step, which is the same
trap on every release that moves composer.lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 02:17:20 -03:00
Ignacio Nelson dad8d21dc8 Translate the update strings into all sixteen locales (#1632)
The English for the updater, the stale-code banner and the activity-log
entry had settled, so this is their pass: 208 lines across sixteen
catalogues, every :placeholder intact, no existing entry reordered.

The activity row and its filter label were modelled on each catalogue's
own "Installed ProjectSend" / "ProjectSend was installed" pair, so the
new rows read like the ones beside them rather than like a different
translator wrote them.

One code change belongs to this pass rather than to the feature: the
update entry recorded "an unrecorded version" when the previous version
was unknown, and context values are substituted into the template
verbatim — never translated — so that sentence fragment would have sat
inside an otherwise Japanese or Polish row forever. It is a dash now.

Verified on a real install with the interface switched to Spanish:
"Sistema — Actualizó ProjectSend a 2.0.3, desde 2.0.2", and the action
filter reading "ProjectSend fue actualizado a una versión nueva".
Scan reports 0 missing in every locale; the Locale tests pass.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 00:13:17 -03:00
Ignacio Nelson 0b994aebe2 Put the update in the activity log (#1631)
The activity log is where an administrator goes to answer "what changed
on this installation, and when" — and the largest change of all was not
in it. A new version arrived, the schema moved, behaviour changed, and
the log said nothing.

`projectsend:update` now records it as a system action, naming both
versions: "Updated ProjectSend to 2.1.0, from 2.0.1". It appears in the
log's own action filter without further work, since that list is built
from the enum.

Only a real version change is written. The container entrypoint runs this
command on every boot, so logging unconditionally would bury the log
under an entry per restart, and a first boot is an installation rather
than an update — SetupCompleted already covers that. "First boot" is
decided by whether any migration had run before this one, not 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. It says "from an unrecorded version", once, ever.

Writing the entry cannot fail the update: an update that worked must not
report failure because its own paperwork did.

Verified on a real manual install — the row renders as
"Updated ProjectSend to 2.0.3, from 2.0.2", attributed to the system.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 00:00:23 -03:00
Ignacio Nelson d30c2ddbb1 Explain why a non-standard port disappears from every link (#1630)
Recent Debian and Ubuntu nginx packages set HTTP_HOST to $host in
fastcgi_params, on purpose — it keeps a client-supplied Host header away
from the application — and $host carries no port. On 80 and 443 nobody
notices. Serve the site on 8080 and every absolute URL the application
builds points at port 80 instead: the login redirect lands somewhere that
does not answer, which reads as ProjectSend being broken rather than as a
web server default having changed.

Found on a manual install of our own, serving on a non-default port,
where the site answered 200 and the browser still got a 404.

The official image is unaffected — Alpine's fastcgi_params has no such
override, so nginx passes the Host header with its port.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 23:04:27 -03:00
Ignacio Nelson 48bc0d4960 Don't abort an update over a symlink's ownership (#1629)
`chown -R` on the install directory also tries to change the ownership of
public/storage, which is a symlink — and some filesystems refuse that even
for root. It turned up on a bind-mounted install (a `fakeowner` mount
refuses lchown outright) and would equally turn up on an NFS export with
root_squash. The update stopped there: files replaced, database not yet
migrated, which is the worst place to stop.

A symlink's own ownership decides nothing, since access is governed by
whatever it points at, so those are now skipped rather than tolerated by
ignoring errors that might have mattered elsewhere.

Found by running the thing on a manual install rather than by reading it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 22:50:14 -03:00
Ignacio Nelson ed0d36de25 Reduce a manual update to one command that asks first (#1628)
Updating a server install cost nine artisan invocations plus a PHP-FPM
reload, written out in three places that had already drifted apart. One
of those steps is silently fatal to skip: with opcache.validate_timestamps
off — what production guides recommend and what our own image ships — the
database moves to the new version while every visitor keeps being served
the old code, and artisan reports the new version throughout.

`sudo ./update.sh` is now the whole procedure. It asks whether to check
GitHub, asks whether to download the release and verifies the checksum
published beside it, and asks whether there is a backup — offering to dump
the database when the answer is no. Then it takes the site down, replaces
the files, runs the update, reloads PHP-FPM, restarts the worker and
brings the site back. The application still has no self-updater: nothing
is fetched or applied unless somebody runs this and answers yes.

Underneath it is `php artisan projectsend:update`, which is everything an
update does that needs no root — and now the only definition of it. Both
container entrypoints call it instead of carrying their own copy of the
sequence, so the two paths cannot drift again.

Three findings worth keeping in the record, all from rehearsing rather
than reasoning:

  - queue:restart has to come last. It writes its signal into the cache,
    so clearing the cache afterwards deletes it and the worker runs old
    code forever.
  - optimize:clear is not safe to recommend. It runs cache:clear, which
    on Redis is FLUSHDB — harmless on the default two-database layout,
    but on a single-database Redis it takes the sessions and the queue
    with it. The compiled caches are cleared individually instead.
  - update.sh overwrites itself mid-run, because the zip contains it and
    bash reads its own script lazily by byte offset. It re-execs from a
    temporary copy before touching anything.

And when the reload is skipped anyway, the application now says so:
projectsend:update records the version it applied, and any staff page
compares that with what the running process actually compiled. The same
check catches the mirror image — new files in place, update never run.

Rehearsed end to end against real installs: a container upgrade (69 to 73
migrations, key and data intact, healthy), a scripted update on a real
nginx + php-fpm install with OPcache pinned (web process moved 2.1.0 to
2.1.1), the skipped-reload case (banner appears naming both versions, and
clears on reload), the refusals (downgrade, non-release zip, truncated
zip, URL passed to --zip, non-root), a database taken down mid-update
(site comes back out of maintenance mode by itself), and a real download
of the published 2.0.0 zip with its checksum verified.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 20:29:20 -03:00
Ignacio Nelson 72749a9070 Document updating, and the OPcache step nobody documents (#1626)
Updating had a numbered list inside INSTALL.md and a code block in the
dashboard, and between them they were missing the step that decides
whether an update works at all.

Rehearsed both paths end to end against a real 2.0.0 install before
writing any of this down. On Docker the whole update is `docker compose
pull && docker compose up -d`: the entrypoint waits for the database,
migrates, ensures the roles, and starts the workers, and it does not
start nginx until that finishes — so a failed migration is a container
that stays down rather than a half-updated site taking traffic. The
generated APP_KEY on the storage volume is left alone. Verified: 69
migrations to 73, key unchanged, data intact, healthcheck healthy.

The manual path is where the gap was. Replace the files, run the four
documented commands, and the site still serves the old version: with
`opcache.validate_timestamps=0` — what every production guide recommends,
and what our own image ships — PHP never re-reads a file it has already
compiled. The database is on the new version, every visitor is on the
old code, and `php artisan` cheerfully reports the new version the whole
time you are trying to work out why. Reproduced exactly that, then
watched a php-fpm reload fix it.

UPDATE.md is now the whole procedure for both, INSTALL.md keeps the short
sequence with the reload added, and the in-app instructions gained the
same line — in the code block, which is not translated, so no locale is
left saying something different.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:42:49 -03:00
Ignacio Nelson 5cc4c6254b Add a dark-mode dashboard screenshot (#1625)
Same frame, same data and the same 1360x850 at 2x as dashboard.png,
captured with the appearance in dark. Not referenced from the README —
it is here for anywhere the dark one suits better.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:39:26 -03:00
Ignacio Nelson 14333d08f1 Frame the dashboard screenshot like its two siblings (#1624)
The retake was 2720x2664 — near square, because it fitted both columns
of widgets end to end. Next to files.png and portal.png, both 16:10, it
read as a different kind of picture and took three times the vertical
room in the README for it.

Same 1360x850 at 2x the other two use. The point was never that every
widget be legible in one frame — only that they all be switched on, so
the installation looks like one somebody uses rather than one nobody has
configured.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:10:30 -03:00
Ignacio Nelson 8e5e73a2cd Retake the dashboard screenshot against a library worth showing (#1623)
The old one photographed a development database: 58 files totalling
7.3 MB, "template-business" and "new-editor" among the largest, two
clients in the storage table, and eight identical "Logged in" rows as
the recent activity. It showed the layout and none of the point.

This one is a plausible installation — real-sounding client
organisations against their quotas, files with names and sizes somebody
would recognise, a month of uploads and downloads with a working week's
rhythm in it, and activity that says what people did rather than that
they arrived. Every widget is on, in the two-column layout, so the
picture also answers "what does this thing actually track".

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:48:37 -03:00
Ignacio Nelson 4f10401c36 Run nginx as the user php-fpm writes as (#1622)
The image creates a fixed-uid www-data for php-fpm but left nginx on the
alpine package's own `user nginx;` (uid 100), while nginx.conf's header
claimed the two already matched. They did not, and the claim is why
nobody looked.

It matters because php-fpm hands nginx files to serve: protected
downloads and thumbnails go out by X-Accel-Redirect. Directories the
application creates on demand come out at Flysystem's private mode, 0700,
owned by www-data — and traversing one of those means being its owner,
since there are no group or other bits to fall back on. So the first
thumbnail an installation ever renders answers 403, from nginx, with
nothing in the application's log to show for it (#1614).

Pointing the directive at www-data is half the fix. /var/lib/nginx and
its tmp/ arrive from the package owned by the old user, and nginx
recreates only the leaf temp directories at boot — as the new user, so
they look correct while their parent stays untraversable. Every request
nginx buffers to disk then fails with a bare 500 that never reaches
php-fpm, which is every upload chunk, which is every upload (#1618).
Fixing one without the other trades a broken thumbnail for a broken
upload, so both land together.

Verified by building the image three ways and driving a real upload and
thumbnail through each: unchanged, the part PUT succeeds and the
thumbnail 403s; with the user directive alone, the thumbnail works and
the part PUT is a bare nginx 500 with "open()
/var/lib/nginx/tmp/client_body/0000000001 failed (13: Permission
denied)"; with both, the chunked upload completes, the thumbnail renders,
and the file downloads back byte for byte.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:55:06 -03:00
Ignacio Nelson 0e3f7d5c68 Take the browser tab's name from the site, not from the build (#1621)
The tab title suffix came from `import.meta.env.VITE_APP_NAME`, with the
starter kit's own `|| 'Laravel'` behind it. Vite resolves that at build
time, and a release ships public/build/ already compiled — precisely so
nobody downstream needs npm — so whatever the build machine had is
frozen for every install that artifact produces. The 2.0.0 zip has
`const s2="Laravel"` in app-BoPagLMd.js, and no APP_KEY, APP_NAME or
VITE_APP_NAME an operator sets afterwards can reach it (#1619).

Swapping the fallback to 'ProjectSend' would fix the reported symptom
and leave the mechanism: a name baked at build time, wrong for anyone
who renamed their site. The name is already on every page — the shared
props carry the site_name setting — so read it from there and drop the
build-time variable entirely, .env.example's dead VITE_APP_NAME line
included.

The root view's own <title> now reads the same shared prop, so the tab
does not show one name before hydration and another after on any
installation whose administrator renamed the site.

Verified in a real browser, which is the only place this is visible —
the server-rendered title was always right, and curl never saw the bug.
Titles read "Log in - ProjectSend" and, after renaming the site with no
rebuild of any kind, "Log in - Acme Files".

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:42:06 -03:00
Ignacio Nelson d53bb9a2f7 Own the application directory in the official image (#1620)
The base php:*-fpm image creates /var/www/html owned by its own www-data
(uid 82) and mode 1777, so that an image can run as an arbitrary user.
This image replaces www-data with a fixed uid 1000 and copies the
release in with COPY --chown — which re-owns what it copies into the
directory, never the directory itself. It was left world-writable,
sticky, and owned by a uid the container no longer has.

fs.protected_symlinks — on by default on Ubuntu, Debian and most current
distributions — then refuses to let a non-root process follow a symlink
in such a directory, and .env is exactly that: the entrypoint keeps it
on the storage volume so a generated APP_KEY survives container
replacement, and links it into place. So every request 503'd with
"ProjectSend is not configured yet" while `docker exec ... cat .env`,
run as root, printed the file back perfectly (#1615).

Three changes, each independently sufficient for the reported case, and
deliberately so — this failure is silent and its symptom points away
from its cause:

  - the image owns /var/www/html as the runtime user, at mode 755;
  - the entrypoint owns the symlink it creates, so it stays followable
    even if that directory's mode ever drifts back;
  - preflight distinguishes "no .env" from ".env is there and cannot be
    read", instead of reporting the second as the first and sending the
    operator off to create a file they already have.

Verified by building the production image before and after: every
request 503s beforehand, with /var/www/html at uid 82 mode 1777 and
www-data denied on the symlink while root reads it; afterwards /up
answers 200, the container reports healthy, and / redirects to /setup.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:20:57 -03:00
Ignacio Nelson 9b265e3b87 Say what a stored upload part's receipt actually is (#1617)
An empty response gets Symfony's default Content-Type, text/html, and a
CDN in front of the app takes that at its word: Cloudflare's Email
Obfuscation and Automatic HTTPS Rewrites both rewrite HTML bodies, so
they drop the origin's ETag from the response — a rewritten body would
no longer match it.

That ETag is the client's only signal that a part landed. Nothing on
this side notices its loss, because LocalPartStore keeps its own record
of every part and complete() never reads a client-supplied one; the
upload simply reaches 100% and stops, with no error at either end.
Reported from a Cloudflare-fronted install (#1616), where the visible
symptom was Uppy's "Could not read the ETag header" — which names CORS,
and sends you chasing a preflight that same-origin requests never make.

Naming the content type accurately keeps the response out of every
HTML-rewriting path there is, rather than asking each CDN-fronted
install to discover this one for itself.

The rationale sits inside the header array rather than above the return:
Scramble reads a comment attached to a return statement as that
response's description in the published OpenAPI document, and this
controller is mounted on the API routes too.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:51:46 -03:00
ignacionelson 377dff7603 Point the lock at community-modules' current commit
The lock pinned a commit that no longer exists: that repository's
history was replaced with a single commit, so an install resolving from
the lock would have failed to fetch it.

Nothing about the package's contents changed — same code, same version
constraint, new commit id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:50:03 -03:00
105 changed files with 7810 additions and 775 deletions
-1
View File
@@ -88,4 +88,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

+75
View File
@@ -13,6 +13,81 @@ 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.
## 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
View File
@@ -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 -3
View File
@@ -8,7 +8,7 @@ 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).
> Getting started with Docker in the first place is covered in [README](README.md#getting-started).
> Installing without Docker, on a plain PHP server, is [INSTALL.md](INSTALL.md).
---
@@ -227,10 +227,15 @@ git pull # or unpack the new release over the directory
docker compose up -d --build
```
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
The app 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.
If you run the published image rather than building your own, it is `docker compose pull` followed
by `docker compose up -d`. Either way, **[UPDATE.md](UPDATE.md)** has the whole procedure: what the
container does on its way up, how to tell it worked, and what to do when it does not.
---
## Moving to another server
+41 -19
View File
@@ -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.
@@ -275,7 +275,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:
@@ -382,8 +385,8 @@ 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`
@@ -405,30 +408,34 @@ of the person who actually downloaded the file. Both are the kind of thing you d
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
config:clear` puts things back immediately, and every update clears it too, saying why. 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.
---
## 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,6 +481,21 @@ 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`.
+78 -22
View File
@@ -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.
---
@@ -151,7 +193,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 +211,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 +262,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
+16 -8
View File
@@ -52,7 +52,7 @@ per-seat pricing. It runs on your server, and the files stay there.
## 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,17 @@ 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 set `ADMIN_EMAIL` and `ADMIN_PASSWORD` in the file first 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 +89,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
View File
@@ -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.
+258
View File
@@ -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 [INSTALL.md](INSTALL.md) (or [DOCKER.md](DOCKER.md))
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 source (DOCKER.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.
+42 -8
View File
@@ -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);
}
}
+3
View File
@@ -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';
@@ -142,6 +143,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',
@@ -242,6 +244,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',
@@ -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',
]);
}
/**
@@ -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);
+25
View File
@@ -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
@@ -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];
}
}
@@ -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);
}
}
@@ -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);
}
}
+74
View File
@@ -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();
}
}
@@ -22,7 +22,9 @@ use App\Modules\Platform\Settings\Settings;
use App\Modules\Platform\Theming\Console\GenerateThemePreviewDataCommand;
use App\Modules\Platform\Theming\EmailThemeRegistry;
use App\Modules\Platform\Theming\PublicThemeRegistry;
use App\Modules\Platform\Scheduling\Console\PurgeFailedJobsCommand;
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 +73,11 @@ class PlatformServiceProvider extends ServiceProvider
$this->commands([
GenerateThemePreviewDataCommand::class,
CheckForUpdatesCommand::class,
UpdateCommand::class,
FetchNewsCommand::class,
DisableCaptchaCommand::class,
TestCaptchaCommand::class,
PurgeFailedJobsCommand::class,
]);
}
}
@@ -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;
}
}
+70 -1
View File
@@ -110,6 +110,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.
@@ -232,6 +244,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 +329,10 @@ enum Setting: string
self::LatestReleaseNotes,
self::LatestReleaseUrl,
self::LatestReleasePublishedAt,
self::AppliedVersion,
self::AppliedVersionAt,
self::UpdateWelcomeFrom,
self::UpdateWelcomeTo,
self::NewsLastFetchedAt,
self::CaptchaProvider,
self::CaptchaKeySource,
@@ -299,7 +352,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 +362,8 @@ enum Setting: string
self::AccountErasureGraceDays,
self::AccountErasureReassignTo,
self::ApiRequestLogRetentionDays,
self::FailedJobRetentionDays,
self::NotificationRetentionDays,
self::ExpiredFilesDeleteAfterDays,
self::CommentsEditWindowMinutes,
self::OrphanFilesDeleteAfterDays,
@@ -369,6 +425,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 +460,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,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 : '';
}
}
+62 -3
View File
@@ -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.
+55
View File
@@ -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);
}
}
+109 -2
View File
@@ -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',
+5
View File
@@ -74,6 +74,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
+1 -7
View File
@@ -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",
@@ -27,7 +21,7 @@
"laravel/tinker": "^2.10.1",
"league/flysystem-aws-s3-v3": "^3.29",
"pragmarx/google2fa": "^9.0",
"projectsend/community-modules": "*",
"projectsend/community-modules": "^1.0",
"stevebauman/purify": "^6.3",
"tightenco/ziggy": "^2.4"
},
Generated
+9 -14
View File
@@ -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": "8aeea042b113838ea5a2a2dcfb0965c5",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -4052,16 +4052,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 +4074,6 @@
"orchestra/testbench": "^10.0",
"pestphp/pest": "^3.8"
},
"default-branch": true,
"type": "library",
"extra": {
"laravel": {
@@ -4088,20 +4087,16 @@
"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/clock",
+5 -1
View File
@@ -39,10 +39,14 @@ return [
'throw' => false,
],
// 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
View File
@@ -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,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 {}
};
+21 -3
View 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.
+33
View File
@@ -66,6 +66,25 @@ 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
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 +99,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
+257
View File
@@ -0,0 +1,257 @@
# 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.
---
## 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: 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
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`. If you set `ADMIN_EMAIL` and `ADMIN_PASSWORD` the first administrator already
exists; otherwise the setup screen creates one.
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
Everything that must outlive the container is on one volume:
```
/var/www/html/storage
```
Uploaded files live there, and so does the generated `.env` holding `APP_KEY`. **Back up that
volume and your MySQL database.** Losing `APP_KEY` makes the SMTP and LDAP passwords stored in your
database undecryptable even if the rest of the backup is perfect.
To keep the data on paths you chose rather than in a named volume, bind-mount a host directory —
the container recreates the directory tree it needs at boot.
A backup nobody has ever restored is a hypothesis, not a backup. Test one, once, on a machine that
is not your live one.
---
## 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).
+15 -7
View File
@@ -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 "$@"
+10 -3
View File
@@ -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/;
+4 -1
View File
@@ -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
View File
@@ -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
];
+84 -34
View File
@@ -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ó",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Denega",
"Deny this request?": "Vols denegar aquesta sol·licitud?",
"Description": "Descripció",
"Description (optional)": "Descripció (opcional)",
"Detach": "Separa",
"Details": "Detalls",
"Disable": "Deshabilitar",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Zamítnout",
"Deny this request?": "Zamítnout tuto žádost?",
"Description": "Popis",
"Description (optional)": "Popis (nepovinné)",
"Detach": "Odpojit",
"Details": "Informace",
"Disable": "Zakázat",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Ablehnen",
"Deny this request?": "Diese Anfrage ablehnen?",
"Description": "Beschreibung",
"Description (optional)": "Beschreibung (optional)",
"Detach": "Trennen",
"Details": "Details",
"Disable": "Deaktivieren",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,6 @@
"Switching Protocols": "Protokollwechsel",
"System": "System",
"System Administrator": "Systemadministrator",
"System settings": "Systemeinstellungen",
"System settings were updated": "Systemeinstellungen wurden aktualisiert",
"System users": "Systembenutzer",
"Tag": "Stichwort",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Denegar",
"Deny this request?": "¿Denegar esta solicitud?",
"Description": "Descripción",
"Description (optional)": "Descripción (opcional)",
"Detach": "Desvincular",
"Details": "Detalles",
"Disable": "Deshabilitar",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Refuser",
"Deny this request?": "Refuser cette demande ?",
"Description": "Description",
"Description (optional)": "Description (facultatif)",
"Detach": "Détacher",
"Details": "Détails",
"Disable": "Désactiver",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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é",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Tolak",
"Deny this request?": "Tolak permintaan ini?",
"Description": "Deskripsi",
"Description (optional)": "Deskripsi (opsional)",
"Detach": "Melepaskan",
"Details": "Detail",
"Disable": "Matikan",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Rifiuta",
"Deny this request?": "Rifiutare questa richiesta?",
"Description": "Descrizione",
"Description (optional)": "Descrizione (facoltativa)",
"Detach": "Scollega",
"Details": "Dettagli",
"Disable": "Disabilita",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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": "行動を促すボタン",
@@ -265,7 +256,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 +301,6 @@
"Deny": "却下",
"Deny this request?": "この申請を却下しますか?",
"Description": "説明",
"Description (optional)": "説明 (任意)",
"Detach": "解除",
"Details": "詳細",
"Disable": "無効化",
@@ -503,7 +492,6 @@
"Login": "ログイン",
"Logout": "ログアウト",
"Loop Detected": "ループを検出",
"Mail provider settings saved.": "メールプロバイダーの設定を保存しました。",
"Mailgun": "Mailgun",
"Maintenance Mode": "メンテナンス中",
"Manage custom fields": "カスタムフィールドの管理",
@@ -572,7 +560,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.": "グループはまだありません。",
@@ -625,8 +612,6 @@
"Orange": "オレンジ",
"Origin Is Unreachable": "オリジンに到達できません",
"Outgoing email": "送信メール",
"Outgoing email theme": "送信メールのテーマ",
"Page :current of :last": ":last ページ中 :current ページ",
"Page Expired": "ページが無効です",
"Pagination Navigation": "ページネーション",
"Partial Content": "部分的なコンテンツ",
@@ -678,7 +663,6 @@
"Public listing settings": "公開一覧の設定",
"Public listing URL": "公開一覧の URL",
"Public pages & portal": "公開ページとポータル",
"Public pages and client portal theme": "公開ページとクライアントポータルのテーマ",
"Published": "公開日",
"Purple": "紫",
"Quota": "上限",
@@ -726,7 +710,6 @@
"Restore": "復元",
"Restore :name": ":nameを復元",
"Restricted extension": "制限された拡張子",
"Result": "結果",
"results": "結果",
"Review membership requests": "参加申請の確認",
"Review pending requests": "保留中の申請を確認",
@@ -789,7 +772,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 +819,6 @@
"Switching Protocols": "プロトコルの切替",
"System": "システム",
"System Administrator": "システム管理者",
"System settings": "システム設定",
"System settings were updated": "システム設定が更新されました",
"System users": "システムユーザー",
"Tag": "タグ",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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 ダウンロードを削除"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Weigeren",
"Deny this request?": "Deze aanvraag weigeren?",
"Description": "Omschrijving",
"Description (optional)": "Omschrijving (optioneel)",
"Detach": "Loskoppelen",
"Details": "Informatie",
"Disable": "Uitschakelen",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Odrzuć",
"Deny this request?": "Odrzucić ten wniosek?",
"Description": "Opis",
"Description (optional)": "Opis (opcjonalnie)",
"Detach": "Odłącz",
"Details": "Szczegóły",
"Disable": "Wyłącz",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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ść",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Recusar",
"Deny this request?": "Recusar esta solicitação?",
"Description": "Descrição",
"Description (optional)": "Descrição (opcional)",
"Detach": "Desanexar",
"Details": "Detalhes",
"Disable": "Desativar",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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": "Призыв к действию",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Отклонить",
"Deny this request?": "Отклонить эту заявку?",
"Description": "Описание",
"Description (optional)": "Описание (необязательно)",
"Detach": "Открепить",
"Details": "Подробности",
"Disable": "Отключить",
@@ -503,7 +492,6 @@
"Login": "Войти",
"Logout": "Выйти",
"Loop Detected": "Обнаружен бесконечный цикл",
"Mail provider settings saved.": "Настройки почтового провайдера сохранены.",
"Mailgun": "Mailgun",
"Maintenance Mode": "Ведутся технические работы",
"Manage custom fields": "Управление дополнительными полями",
@@ -572,7 +560,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.": "Групп пока нет.",
@@ -625,8 +612,6 @@
"Orange": "Оранжевый",
"Origin Is Unreachable": "Источник недоступен",
"Outgoing email": "Исходящая почта",
"Outgoing email theme": "Тема исходящих писем",
"Page :current of :last": "Страница :current из :last",
"Page Expired": "Страница устарела",
"Pagination Navigation": "Навигация",
"Partial Content": "Не полное содержимое",
@@ -678,7 +663,6 @@
"Public listing settings": "Настройки публичного списка",
"Public listing URL": "Адрес публичного списка",
"Public pages & portal": "Публичные страницы и портал",
"Public pages and client portal theme": "Тема публичных страниц и клиентского портала",
"Published": "Опубликовано",
"Purple": "Фиолетовый",
"Quota": "Квота",
@@ -726,7 +710,6 @@
"Restore": "Восстановить",
"Restore :name": "Восстановить :name",
"Restricted extension": "Ограниченное расширение",
"Result": "Результат",
"results": "результатов",
"Review membership requests": "Рассмотрение заявок на членство",
"Review pending requests": "Рассмотреть ожидающие заявки",
@@ -789,7 +772,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 +819,6 @@
"Switching Protocols": "Переключение протоколов",
"System": "Система",
"System Administrator": "Системный администратор",
"System settings": "Системные настройки",
"System settings were updated": "Системные настройки обновлены",
"System users": "Пользователи системы",
"Tag": "Тег",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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-архивы для скачивания"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,6 @@
"Deny": "Kataa",
"Deny this request?": "Kataa ombi hili?",
"Description": "Maelezo",
"Description (optional)": "Maelezo (si lazima)",
"Detach": "Tenganisha",
"Details": "Maelezo",
"Disable": "Zima",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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ı",
@@ -265,7 +256,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 +301,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",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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",
@@ -265,7 +256,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 +301,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",
@@ -503,7 +492,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 +560,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.",
@@ -625,8 +612,6 @@
"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",
@@ -678,7 +663,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 +710,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ờ",
@@ -789,7 +772,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 +819,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",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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"
}
+84 -34
View File
@@ -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": "行动按钮",
@@ -265,7 +256,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 +301,6 @@
"Deny": "拒绝",
"Deny this request?": "要拒绝此申请吗?",
"Description": "说明",
"Description (optional)": "说明(可选)",
"Detach": "分离",
"Details": "详情",
"Disable": "禁用",
@@ -503,7 +492,6 @@
"Login": "登录",
"Logout": "登出",
"Loop Detected": "检测到环路",
"Mail provider settings saved.": "邮件服务商设置已保存。",
"Mailgun": "Mailgun",
"Maintenance Mode": "服务不可用",
"Manage custom fields": "管理自定义字段",
@@ -572,7 +560,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.": "还没有群组。",
@@ -625,8 +612,6 @@
"Orange": "橙色",
"Origin Is Unreachable": "原点无法到达",
"Outgoing email": "发出的邮件",
"Outgoing email theme": "发出邮件的主题",
"Page :current of :last": "第 :current 页,共 :last 页",
"Page Expired": "页面会话已超时",
"Pagination Navigation": "分页导航",
"Partial Content": "部分内容",
@@ -678,7 +663,6 @@
"Public listing settings": "公开列表设置",
"Public listing URL": "公开列表网址",
"Public pages & portal": "公开页面与门户",
"Public pages and client portal theme": "公开页面和客户门户的主题",
"Published": "发布时间",
"Purple": "紫色",
"Quota": "配额",
@@ -726,7 +710,6 @@
"Restore": "恢复",
"Restore :name": "恢复:name",
"Restricted extension": "受限扩展名",
"Result": "结果",
"results": "结果",
"Review membership requests": "审核成员申请",
"Review pending requests": "查看待处理的申请",
@@ -789,7 +772,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 +819,6 @@
"Switching Protocols": "切换协议",
"System": "系统",
"System Administrator": "系统管理员",
"System settings": "系统设置",
"System settings were updated": "系统设置已更新",
"System users": "系统用户",
"Tag": "标签",
@@ -874,7 +855,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 +868,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 +891,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 +899,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 +939,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 +1040,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 +1159,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 +1230,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 +1300,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 +1313,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 +1700,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 +1732,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 +1749,88 @@
"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 下载"
}
+29 -2
View File
@@ -1,6 +1,6 @@
import '../css/app.css';
import { createInertiaApp } from '@inertiajs/react';
import { createInertiaApp, router } from '@inertiajs/react';
import axios from 'axios';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createRoot } from 'react-dom/client';
@@ -19,7 +19,23 @@ declare global {
// every write 419s the moment a neighbour answers a request.
axios.defaults.xsrfCookieName = xsrfCookieName();
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
/**
* The suffix on every browser tab title.
*
* Read from the shared props — the site name an administrator set — and
* never from `import.meta.env`. Vite resolves those at build time, and the
* published release ships `public/build/` already compiled, so whatever a
* build machine happened to have is frozen for every install downstream and
* no setting can move it afterwards. That is how 2.0.0 came to tell every
* visitor its tabs were "Laravel".
*/
let appName = 'ProjectSend';
const siteName = (page: { props: Record<string, unknown> }): string | null => {
const name = page.props.name;
return typeof name === 'string' && name !== '' ? name : null;
};
// Pages shipped by packages, re-keyed to look exactly like a host page
// (`./pages/<name>.tsx`) so resolvePageComponent finds them the same
@@ -43,6 +59,10 @@ createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, { ...packagePages, ...import.meta.glob('./pages/**/*.tsx') }),
setup({ el, App, props }) {
// Before the first render, so the title callback below already has
// it when the initial page produces its <Head>.
appName = siteName(props.initialPage) ?? appName;
const root = createRoot(el);
root.render(<App {...props} />);
@@ -52,5 +72,12 @@ createInertiaApp({
},
});
// Renaming the site is a settings save like any other, so the new name
// arrives on the very next visit — without this the tabs would keep the
// old one until someone reloaded the page.
router.on('navigate', (event) => {
appName = siteName(event.detail.page) ?? appName;
});
// This will set light / dark mode on load...
initializeTheme();
@@ -0,0 +1,85 @@
import { usePage } from '@inertiajs/react';
import { AlertTriangle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { useTranslation } from '@/hooks/use-translation';
import { type SharedData } from '@/types';
/**
* Says out loud that this server is not running the code it was updated
* to — the one update failure that has no other symptom.
*
* With OPcache configured the way production guides recommend, replacing
* the files and reloading nothing leaves the database on the new version
* and every visitor on the old code, indefinitely and without an error
* anywhere. `projectsend:update` records what it applied; this compares it
* with what the process rendering this page actually compiled.
*
* Deliberately on every staff page rather than the dashboard alone: the
* person who missed the reload has no reason to suspect anything, so the
* notice has to find them rather than wait to be visited.
*/
export function CodeNoticeBanner() {
const { t } = useTranslation();
const { code_notice: notice } = usePage<SharedData>().props;
if (!notice) {
return null;
}
const container = notice.install_kind === 'container';
// Stated as a fact first, cause second. A deliberate rollback lands
// here too, and telling somebody to reload PHP-FPM when they meant to
// go back a version would send them the wrong way.
const title =
notice.reason === 'stale_code'
? t('This server is running version :running, but version :applied is installed', {
running: notice.running,
applied: notice.applied,
})
: t('Version :running is installed, but the update has not been run', { running: notice.running });
return (
<Alert variant={notice.reason === 'stale_code' ? 'destructive' : 'warning'}>
<AlertTriangle className="size-4" />
<AlertTitle>{title}</AlertTitle>
<AlertDescription className="space-y-2">
{notice.reason === 'stale_code' ? (
<>
<p>
{container
? t('The database was updated to :applied, and this container is still running the code it started with.', {
applied: notice.applied,
})
: t(
'The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.',
{ applied: notice.applied },
)}
</p>
<Command>{container ? 'docker compose up -d --force-recreate' : 'sudo systemctl reload php8.4-fpm'}</Command>
{!container && (
<p className="text-xs">
{t('If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.')}
</p>
)}
</>
) : (
<>
<p>
{t(
'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: notice.applied },
)}
</p>
<Command>{container ? 'docker compose up -d --force-recreate' : 'sudo ./update.sh'}</Command>
</>
)}
</AlertDescription>
</Alert>
);
}
function Command({ children }: { children: string }) {
return <code className="bg-background/60 inline-block rounded px-2 py-1 font-mono text-xs">{children}</code>;
}
@@ -0,0 +1,50 @@
import { type SharedData } from '@/types';
import { usePage } from '@inertiajs/react';
import { MessagesSquare } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTranslation } from '@/hooks/use-translation';
/**
* The invitation to the community, on both of the pages that greet an
* administrator — after an install and after an update.
*
* One component because it was two identical copies within an hour of
* each other, and the pair would have drifted the first time either page
* was touched alone. Both are the same offer made at the same kind of
* moment, so they should keep looking like it.
*
* Carried on `accent` rather than `muted` so it reads as a feature
* instead of a footnote. That token is the brand colour at surface
* strength and already has a dark-mode counterpart, so this stays legible
* in both without a hardcoded purple — and follows the palette on an
* installation whose branding replaces ours.
*/
export default function DiscordInvitation() {
const { t } = useTranslation();
const { links } = usePage<SharedData>().props;
return (
<div className="bg-accent border-primary/20 flex flex-col items-center gap-4 rounded-lg border p-6 text-center">
<MessagesSquare className="text-primary size-8" strokeWidth={1.5} />
<div className="space-y-1">
<p className="text-accent-foreground font-medium">{t('Come and say hello')}</p>
<p className="text-accent-foreground/80 text-sm">
{t(
'ProjectSend has a Discord: release news, help when something is not behaving, and other people running the same software. We are in it too.',
)}
</p>
</div>
{/* Solid rather than outline: an outline button's hover state is
this exact background colour, so on this card it would
disappear under the cursor. */}
<Button asChild>
<a href={links.discord} target="_blank" rel="noreferrer">
{t('Join the Discord')}
</a>
</Button>
</div>
);
}
@@ -0,0 +1,41 @@
import { useTranslation } from '@/hooks/use-translation';
/**
* The line from projectsend.org, on the two pages that greet somebody —
* the only two screens in the application where it is worth saying who
* made this rather than getting on with the work.
*
* Drawn rather than set as the 🇦🇷 emoji the website uses. Flag emoji are
* regional-indicator pairs, and neither Windows nor most Linux desktops
* ship glyphs for them: both fall back to a pair of small letters, so a
* good half of the people self-hosting this would read "AR Made with care
* in Argentina" (which is exactly what the first render of this component
* did). Eleven lines of SVG look the same on every machine.
*
* The Sun of May is a filled disc here on purpose — at sixteen pixels its
* thirty-two rays are a smudge, and a clean circle reads as the flag
* while a smudge reads as a rendering fault.
*/
function ArgentineFlag() {
return (
<svg viewBox="0 0 24 16" className="inline-block h-3 w-[1.125rem] rounded-[2px] align-[-1px] ring-1 ring-black/10" aria-hidden="true">
<rect width="24" height="16" fill="#74ACDF" />
<rect y="5.33" width="24" height="5.33" fill="#FFFFFF" />
<circle cx="12" cy="8" r="1.6" fill="#F6B40E" />
</svg>
);
}
export default function MadeInArgentina() {
const { t } = useTranslation();
return (
<p className="text-muted-foreground/80 flex items-center justify-center gap-1.5 text-center text-xs">
<ArgentineFlag />
{/* The flag is not language, so it stays out of the translated
string — a translator carrying an image through sixteen
catalogues will eventually drop one. */}
{t('Made with care in Argentina. Shared with the world.')}
</p>
);
}
+24 -19
View File
@@ -1,3 +1,4 @@
import { UpdateOptionsDialog } from '@/components/update-options-dialog';
import { useTranslation } from '@/hooks/use-translation';
import { cn } from '@/lib/utils';
@@ -14,14 +15,15 @@ export type InstallKind = 'container' | 'manual';
* have, for a stack they are not using, at the exact moment they were trying
* to do the right thing.
*
* `compact` is for the dashboard card, a narrow column beside other widgets:
* the manual sequence is seven lines and would swamp it, so there it points
* at the dialog, which has the room. `codeClassName` exists for the same
* caller — its code sits inside a warning alert and has to match it.
* `compact` is for the dashboard card, a narrow column beside other widgets.
* `codeClassName` exists for the same caller — its code sits inside a warning
* alert and has to match it.
*
* The full manual sequence is deliberately the whole thing, backup included.
* It is the order INSTALL.md gives, and the step people skip is the one that
* matters: migrations move forwards, not backwards.
* This used to print the whole nine-command sequence, which was a third copy
* of something that also lived in INSTALL.md and UPDATE.md and had already
* drifted from both. It is now one script, and the reason it can be is that
* projectsend:update owns every part of an update that does not need root —
* see UpdateInstallation.
*/
export function UpdateInstructions({
kind,
@@ -44,27 +46,30 @@ export function UpdateInstructions({
if (compact) {
return (
<p className="text-muted-foreground">
{t('To update, follow the steps in INSTALL.md — back up first, then unpack the release and run the migrations.')}
</p>
<div className="text-muted-foreground space-y-1">
<p>{t('To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.')}</p>
<UpdateOptionsDialog />
</div>
);
}
return (
<>
<p className="text-muted-foreground mb-1">{t('To update, back up your database and files, then:')}</p>
<p className="text-muted-foreground mb-1">{t('To update, run this in the install directory:')}</p>
<code className={cn('block rounded px-2 py-1.5 whitespace-pre-wrap', codeClassName ?? 'bg-muted')}>
{[
'php artisan down',
'# unpack the new release over this directory, keeping .env and storage/',
'php artisan migrate --force',
'php artisan projectsend:ensure-roles',
'php artisan optimize:clear',
'php artisan queue:restart',
'php artisan up',
'cd /var/www/projectsend',
'sudo ./update.sh',
].join('\n')}
</code>
<p className="text-muted-foreground mt-1 text-xs">{t('Run these as the user your web server runs as. INSTALL.md has the full procedure.')}</p>
<p className="text-muted-foreground mt-1 text-xs">
{t(
'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.',
)}
</p>
<div className="mt-1">
<UpdateOptionsDialog />
</div>
</>
);
}
@@ -0,0 +1,72 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useTranslation } from '@/hooks/use-translation';
/**
* The handful of update.sh options worth knowing about, behind a link
* rather than printed on the page.
*
* The screen that tells somebody how to update has one job: the one
* command. Listing every flag beside it would bury that command under
* options most people never need — but two of them are the ones an
* administrator most wants at exactly that moment ("can it back up for
* me?", "can I just look?"), and finding them meant opening UPDATE.md on
* a machine they are probably not sitting at.
*
* So: nothing on screen until asked for, and then four lines. The script
* documents the rest with --help, and UPDATE.md documents the procedure.
*
* It opens from inside the update dialog as well as from the dashboard
* card. A dialog on top of a dialog is unusual, and correct here — the
* reader asked for a footnote to what they are already reading, and
* Escape returns them to it.
*/
export function UpdateOptionsDialog() {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const options: { flag: string; description: string }[] = [
{ flag: '--backup', description: t('Dump the database before touching anything, and stop if the dump fails.') },
{ flag: '--check', description: t('Report which version is installed and which is available, and change nothing.') },
{ flag: '--i-have-a-backup', description: t('Skip the question about backups, for a run you are not sitting in front of.') },
{ flag: '--no-restart', description: t('Leave PHP and the worker alone, for when you restart them yourself.') },
];
return (
<>
<button type="button" onClick={() => setOpen(true)} className="text-muted-foreground text-xs underline hover:no-underline">
{t('Other options')}
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{t('Update options')}</DialogTitle>
</DialogHeader>
<div className="space-y-3 text-sm">
<p className="text-muted-foreground">
{t('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.')}
</p>
<dl className="divide-y">
{options.map((option) => (
<div key={option.flag} className="grid gap-1 py-2 sm:grid-cols-[10rem_1fr] sm:gap-3">
<dt>
<code className="bg-muted rounded px-1.5 py-0.5 text-xs">{option.flag}</code>
</dt>
<dd className="text-muted-foreground">{option.description}</dd>
</div>
))}
</dl>
<p className="text-muted-foreground text-xs">
{t('sudo ./update.sh --help lists every option, and UPDATE.md walks through the whole procedure, including doing it by hand.')}
</p>
</div>
</DialogContent>
</Dialog>
</>
);
}
@@ -2,6 +2,7 @@ import { AppContent } from '@/components/app-content';
import { AppShell } from '@/components/app-shell';
import { AppSidebar } from '@/components/app-sidebar';
import { AppSidebarHeader } from '@/components/app-sidebar-header';
import { CodeNoticeBanner } from '@/components/code-notice-banner';
import { Toaster } from '@/components/toaster';
import { type BreadcrumbItem } from '@/types';
@@ -11,6 +12,13 @@ export default function AppSidebarLayout({ children, breadcrumbs = [] }: { child
<AppSidebar />
<AppContent variant="sidebar">
<AppSidebarHeader breadcrumbs={breadcrumbs} />
{/* Renders nothing unless this process is running code the
installation was not updated to — see RunningCodeState.
Here rather than on the dashboard because the person it
is for has no reason to go looking. */}
<div className="px-6 pt-4 empty:hidden md:px-4">
<CodeNoticeBanner />
</div>
{children}
</AppContent>
<Toaster />
+35 -4
View File
@@ -1,8 +1,9 @@
import { type BreadcrumbItem, type SharedData } from '@/types';
import { Head, usePage } from '@inertiajs/react';
import { Head, Link, usePage } from '@inertiajs/react';
import Heading from '@/components/heading';
import ProjectSendLogo from '@/components/projectsend-logo';
import { useFormatDate } from '@/hooks/use-format-date';
import { useTranslation } from '@/hooks/use-translation';
import AppLayout from '@/layouts/app-layout';
@@ -14,10 +15,17 @@ interface Environment {
database: string;
}
interface LastUpdate {
version: string;
at: string;
}
interface AboutProps {
license: string;
/** Null for anyone the dashboard's System widget would also hide it from. */
environment: Environment | null;
/** Null on an installation that has never been updated through the command. */
updated: LastUpdate | null;
}
function Row({ label, value }: { label: string; value: string }) {
@@ -29,8 +37,9 @@ function Row({ label, value }: { label: string; value: string }) {
);
}
export default function About({ license, environment }: AboutProps) {
export default function About({ license, environment, updated }: AboutProps) {
const { t } = useTranslation();
const { dateTime } = useFormatDate();
const { version, edition, links } = usePage<SharedData>().props;
const breadcrumbs: BreadcrumbItem[] = [{ title: t('About'), href: '/system/about' }];
@@ -67,9 +76,27 @@ export default function About({ license, environment }: AboutProps) {
<a href={links.source} target="_blank" rel="noreferrer" className="underline hover:no-underline">
{t('Source code')}
</a>
<a href={links.open_collective} target="_blank" rel="noreferrer" className="underline hover:no-underline">
{t('Support the project')}
{/* Absent on a managed installation, where the reader
is already paying for this — see OfficialLinks. */}
{links.open_collective && (
<a href={links.open_collective} target="_blank" rel="noreferrer" className="underline hover:no-underline">
{t('Support the project')}
</a>
)}
<a href={links.discord} target="_blank" rel="noreferrer" className="underline hover:no-underline">
{t('Discord')}
</a>
<Link href={route('system.getting-started')} className="underline hover:no-underline">
{t('Getting started')}
</Link>
{/* Same gate as the environment block below, which is
why it rides on the same prop: the page it links to
answers the question this one starts. */}
{environment && (
<Link href={route('system.whats-new')} className="underline hover:no-underline">
{t("What's new")}
</Link>
)}
</div>
{environment && (
@@ -80,6 +107,10 @@ export default function About({ license, environment }: AboutProps) {
</p>
<dl className="divide-y text-sm">
<Row label={t('Version')} value={environment.version} />
{/* Absent until this installation has been
updated at least once through the command —
a fresh install has no update to date. */}
{updated && <Row label={t('Updated')} value={t(':version on :date', { version: updated.version, date: dateTime(updated.at) })} />}
<Row label={t('Edition')} value={environment.edition} />
<Row label="PHP" value={environment.php} />
<Row label="Laravel" value={environment.laravel} />
@@ -0,0 +1,161 @@
import { type BreadcrumbItem, type SharedData } from '@/types';
import { Head, Link, usePage } from '@inertiajs/react';
import { ArrowRight, Boxes, Check, Clock, Contact, MailOpen, Palette, Send, Upload, Users, type LucideIcon } from 'lucide-react';
import DiscordInvitation from '@/components/discord-invitation';
import MadeInArgentina from '@/components/made-in-argentina';
import { Button } from '@/components/ui/button';
import { useTranslation } from '@/hooks/use-translation';
import AppLayout from '@/layouts/app-layout';
interface QuickStartItem {
key: string;
title: string;
description: string;
href: string;
/** The installation does not really work without this one. */
essential: boolean;
/** True only where the database can answer it — see QuickStart. */
done: boolean;
}
interface GettingStartedProps {
items: QuickStartItem[];
/** False once the page has been read, or when opened from a link later. */
justInstalled: boolean;
}
/**
* Borrowed from the sidebar's own vocabulary wherever there is one, so
* the icon on a card is the icon on the screen it opens — see
* app-sidebar.tsx. Icons are presentation, so they are chosen here rather
* than sent over the wire by QuickStart.
*/
const ICONS: Record<string, LucideIcon> = {
client: Contact,
upload: Upload,
group: Boxes,
theme: Palette,
'email-theme': MailOpen,
email: Send,
team: Users,
scheduler: Clock,
};
export default function GettingStarted({ items, justInstalled }: GettingStartedProps) {
const { t } = useTranslation();
const { version, edition } = usePage<SharedData>().props;
// Nobody on a managed installation installed anything, and the version
// they are running is not theirs to keep or to change — it is looked
// after for them. Thanking them for installing it would be thanking
// them for somebody else's work, on the one screen whose whole job is
// to sound like a person wrote it.
const managed = edition === 'cloud';
const breadcrumbs: BreadcrumbItem[] = [{ title: t('Getting started'), href: '/system/getting-started' }];
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title={t('Getting started')} />
<div className="px-4 py-6">
<div className="mx-auto max-w-3xl space-y-8">
<div className="space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
{justInstalled
? managed
? t('Thank you for choosing ProjectSend')
: t('Thank you for installing ProjectSend')
: t('Getting started')}
</h1>
<p className="text-muted-foreground text-sm">
{justInstalled
? managed
? t('Your site is ready, and keeping it running is our job. Here is what is worth doing first.')
: t('You are running version :version, and it is yours to keep. Here is what is worth doing first.', {
version,
})
: t('The short version of what to do on a new installation. Nothing here expires.')}
</p>
</div>
{/* Two columns, so eight steps are four rows and the whole
page is one screen. One column on a phone. */}
<ul className="grid gap-3 sm:grid-cols-2">
{items.map((item) => (
<li key={item.key}>
<QuickStartCard item={item} />
</li>
))}
</ul>
{/* Last, deliberately. Somebody who has just installed this
came here with a job in mind, and the fastest way to
lose them is to lead with a social invitation. It is
still worth making — after the work, not in front of
it. */}
<DiscordInvitation />
<div className="space-y-6">
<div className="flex justify-center">
<Button asChild variant="outline">
<Link href={route('dashboard')}>
{t('Continue to the dashboard')}
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
<MadeInArgentina />
</div>
</div>
</div>
</AppLayout>
);
}
function QuickStartCard({ item }: { item: QuickStartItem }) {
const { t } = useTranslation();
const Icon = ICONS[item.key] ?? ArrowRight;
// Three states, each on a colour this application already uses for
// exactly this meaning: green for done (the --success token behind
// Badge's success variant), amber for "attend to this" (the warning
// Alert variant, verbatim), and the brand colour for everything else.
// Amber rather than red throughout: none of these is broken, one of
// them is just more urgent than the others.
const chip = item.done
? 'bg-success/10 text-success'
: item.essential
? 'bg-amber-100 text-amber-600 dark:bg-amber-900/40 dark:text-amber-500'
: 'bg-accent text-primary';
return (
<Link
href={item.href}
className="hover:border-primary/40 hover:bg-accent/40 group flex h-full items-start gap-3 rounded-lg border p-4 transition-colors"
>
<span className={`flex size-9 shrink-0 items-center justify-center rounded-lg ${chip}`}>
{item.done ? <Check className="size-4" strokeWidth={3} /> : <Icon className="size-4" strokeWidth={2} />}
</span>
<span className="min-w-0 flex-1">
<span className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className={`font-medium ${item.done ? 'text-muted-foreground line-through' : ''}`}>{item.title}</span>
{item.essential && !item.done && (
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold tracking-wide text-amber-700 uppercase dark:bg-amber-900/40 dark:text-amber-500">
{t('Essential')}
</span>
)}
</span>
<span className="text-muted-foreground block text-sm">{item.description}</span>
</span>
<ArrowRight className="text-muted-foreground/40 group-hover:text-primary mt-1 size-4 shrink-0" />
</Link>
);
}
+61 -8
View File
@@ -1,14 +1,17 @@
import { type BreadcrumbItem, type SharedData } from '@/types';
import { Head, Link, useForm, usePage } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Head, Link, router, useForm, usePage } from '@inertiajs/react';
import { FormEventHandler, useState } from 'react';
import Heading from '@/components/heading';
import InputError from '@/components/input-error';
import { SaveButton } from '@/components/save-button';
import { TestResultAlert } from '@/components/test-result-alert';
import { TimezonePicker, type TimezoneOption } from '@/components/timezone-picker';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useFormatDate } from '@/hooks/use-format-date';
import { useTranslation } from '@/hooks/use-translation';
import AppLayout from '@/layouts/app-layout';
@@ -20,6 +23,10 @@ interface SystemSettingsProps {
viewer_timezone: string | null;
can_manage_updates: boolean;
check_for_updates: boolean | null;
/** When the release feed was last asked, by anybody. Null until it has been. */
last_checked_at: string | null;
/** The answer to a "check now" press, for the one render after it. */
check_result: { ok: boolean; message: string } | null;
}
export default function SystemSettings({
@@ -29,9 +36,25 @@ export default function SystemSettings({
viewer_timezone,
can_manage_updates,
check_for_updates,
last_checked_at,
check_result,
}: SystemSettingsProps) {
const { t } = useTranslation();
const { dateTime } = useFormatDate();
const { version, links } = usePage<SharedData>().props;
const [checking, setChecking] = useState(false);
const checkNow = () => {
router.post(
route('system-settings.check-for-updates'),
{},
{
preserveScroll: true,
onStart: () => setChecking(true),
onFinish: () => setChecking(false),
},
);
};
// Matched against the offered list so the note names the zone the way
// the picker does, rather than printing a raw identifier.
@@ -121,9 +144,26 @@ export default function SystemSettings({
</div>
<p className="text-muted-foreground text-sm">
{t(
"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.",
"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.",
)}
</p>
{/* Outside the form on purpose: this asks a
question rather than saving anything, and
pressing it should not depend on — or
discard — unsaved edits above it. */}
<div className="mt-1 flex flex-wrap items-center gap-3">
<Button type="button" variant="outline" onClick={checkNow} disabled={checking}>
{checking ? t('Checking…') : t('Check now')}
</Button>
{last_checked_at && (
<span className="text-muted-foreground text-sm">
{t('Last checked :date', { date: dateTime(last_checked_at) })}
</span>
)}
</div>
{check_result && <TestResultAlert ok={check_result.ok}>{check_result.message}</TestResultAlert>}
</div>
)}
@@ -132,13 +172,26 @@ export default function SystemSettings({
<p className="text-muted-foreground/70 mt-10 text-xs">
ProjectSend {version} ·{' '}
{/* The host, not a hardcoded "projectsend.org": each
edition has its own front door and this line used to
name the wrong one on the hosted service. */}
<a href={links.website} target="_blank" rel="noreferrer" className="hover:text-muted-foreground underline">
projectsend.org
</a>{' '}
·{' '}
<a href={links.open_collective} target="_blank" rel="noreferrer" className="hover:text-muted-foreground underline">
{t('Support the project')}
{new URL(links.website).host.replace(/^www\./, '')}
</a>
{links.open_collective && (
<>
{' '}
·{' '}
<a
href={links.open_collective}
target="_blank"
rel="noreferrer"
className="hover:text-muted-foreground underline"
>
{t('Support the project')}
</a>
</>
)}
</p>
</div>
</AppLayout>
@@ -1,12 +1,17 @@
import { type BreadcrumbItem } from '@/types';
import { Head, Link, router } from '@inertiajs/react';
import { Head, Link, router, useForm } from '@inertiajs/react';
import { type FormEventHandler } from 'react';
import { ConfirmDialog } from '@/components/confirm-dialog';
import Heading from '@/components/heading';
import HeadingSmall from '@/components/heading-small';
import InputError from '@/components/input-error';
import { Pagination, type PaginationMeta } from '@/components/pagination';
import { SaveButton } from '@/components/save-button';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useFormatDate } from '@/hooks/use-format-date';
import { useTranslation } from '@/hooks/use-translation';
import AppLayout from '@/layouts/app-layout';
@@ -16,6 +21,8 @@ interface ScheduledTask {
label: string;
status: 'success' | 'failed' | null;
message: string | null;
/** What the task found, for the tasks that find something. Never set on a failure. */
detail: string | null;
duration_ms: number | null;
ran_at: string | null;
}
@@ -37,6 +44,8 @@ interface SchedulerSettingsProps {
failed_pagination: PaginationMeta;
failed_total: number;
pending_jobs_count: number;
/** Days each self-growing table is kept for. 0 means indefinitely. */
retention: { failed_jobs: number; notifications: number };
}
export default function SchedulerSettings({
@@ -46,10 +55,23 @@ export default function SchedulerSettings({
failed_pagination,
failed_total,
pending_jobs_count,
retention,
}: SchedulerSettingsProps) {
const { t } = useTranslation();
const { dateTime } = useFormatDate();
// Strings rather than numbers: an emptied number input is '', and
// coercing that to 0 mid-typing would silently mean "keep everything".
const { data, setData, patch, processing, errors, recentlySuccessful } = useForm({
failed_jobs: String(retention.failed_jobs),
notifications: String(retention.notifications),
});
const saveRetention: FormEventHandler = (event) => {
event.preventDefault();
patch(route('system-settings.scheduler.retention'), { preserveScroll: true });
};
const breadcrumbs: BreadcrumbItem[] = [
{ title: t('Settings'), href: '/system/settings' },
{ title: t('Scheduler'), href: '/system/settings/scheduler' },
@@ -130,7 +152,10 @@ export default function SchedulerSettings({
<td className="text-muted-foreground px-4 py-2.5 whitespace-nowrap">
{task.duration_ms !== null ? `${task.duration_ms} ms` : '—'}
</td>
<td className="text-muted-foreground max-w-xs px-4 py-2.5 break-words">{task.message ?? '—'}</td>
{/* A failure's own message wins: what the
last successful run found is not the
answer to why this one broke. */}
<td className="text-muted-foreground max-w-xs px-4 py-2.5 break-words">{task.message ?? task.detail ?? '—'}</td>
</tr>
))}
</tbody>
@@ -138,6 +163,57 @@ export default function SchedulerSettings({
</div>
)}
{tab === 'tasks' && (
<form onSubmit={saveRetention} className="max-w-xl space-y-4">
<HeadingSmall
title={t('Housekeeping')}
description={t('Two things pile up on their own. These are the windows the daily purges above work to.')}
/>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-1.5">
<Label htmlFor="failed_jobs_retention">{t('Keep failed jobs for')}</Label>
<div className="flex items-center gap-2">
<Input
id="failed_jobs_retention"
type="number"
min={0}
max={3650}
value={data.failed_jobs}
onChange={(event) => setData('failed_jobs', event.target.value)}
className="w-28"
/>
<span className="text-muted-foreground text-sm">{t('days')}</span>
</div>
<InputError message={errors.failed_jobs} />
</div>
<div className="grid gap-1.5">
<Label htmlFor="notifications_retention">{t('Keep read notifications for')}</Label>
<div className="flex items-center gap-2">
<Input
id="notifications_retention"
type="number"
min={0}
max={3650}
value={data.notifications}
onChange={(event) => setData('notifications', event.target.value)}
className="w-28"
/>
<span className="text-muted-foreground text-sm">{t('days')}</span>
</div>
<InputError message={errors.notifications} />
</div>
</div>
<p className="text-muted-foreground text-sm">
{t('0 keeps everything. Unread notifications are never deleted, however old they are.')}
</p>
<SaveButton processing={processing} recentlySuccessful={recentlySuccessful} />
</form>
)}
{tab === 'failed' && (
<div>
<div className="flex items-start justify-between gap-4">
@@ -83,7 +83,11 @@ function ThemeCard({
export default function ThemingSettings({ theme, email_theme, themes, email_themes }: ThemingSettingsProps) {
const { t } = useTranslation();
const [tab, setTab] = useState<Tab>('pages');
// ?tab=email opens on the email themes, so something elsewhere can link
// at one of these two halves rather than at the page and a sentence
// asking the reader to find the right tab. Anything unrecognised falls
// back to the pages tab rather than rendering nothing.
const [tab, setTab] = useState<Tab>(new URLSearchParams(window.location.search).get('tab') === 'email' ? 'email' : 'pages');
const [activatingKey, setActivatingKey] = useState<string | null>(null);
const [previewing, setPreviewing] = useState<ThemeOption | null>(null);
+141
View File
@@ -0,0 +1,141 @@
import { type BreadcrumbItem } from '@/types';
import { Head, Link } from '@inertiajs/react';
import { ArrowRight, CircleCheck } from 'lucide-react';
import DiscordInvitation from '@/components/discord-invitation';
import MadeInArgentina from '@/components/made-in-argentina';
import { Button } from '@/components/ui/button';
import { useTranslation } from '@/hooks/use-translation';
import AppLayout from '@/layouts/app-layout';
interface ReleaseItem {
/** The bold lead-in of a changelog bullet; empty when it has none. */
title: string;
body: string;
}
interface ReleaseGroup {
/** "Added", "Improved", … — empty for a list written without one. */
heading: string;
items: ReleaseItem[];
}
interface Release {
version: string;
date: string;
intro: string[];
groups: ReleaseGroup[];
}
interface WhatsNewProps {
version: string;
/** Empty when the previous version was never recorded. */
previousVersion: string;
/** False when the page is opened from a link rather than after an update. */
justUpdated: boolean;
releases: Release[];
}
export default function WhatsNew({ version, previousVersion, justUpdated, releases }: WhatsNewProps) {
const { t } = useTranslation();
const breadcrumbs: BreadcrumbItem[] = [{ title: t("What's new"), href: '/system/whats-new' }];
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title={t("What's new")} />
<div className="px-4 py-6">
<div className="mx-auto max-w-2xl space-y-10">
<div className="space-y-3 text-center">
<CircleCheck className="text-primary mx-auto size-12" strokeWidth={1.5} />
<h1 className="text-2xl font-semibold tracking-tight">
{justUpdated ? t('Thank you for updating ProjectSend') : t('ProjectSend :version', { version })}
</h1>
{/* Warm while it is a greeting, plain once it is a
reference: somebody who opened this from a link
months later is looking something up, and being
thanked again would be noise. */}
<p className="text-muted-foreground text-sm">
{justUpdated
? previousVersion !== ''
? t(
'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: previousVersion },
)
: t(
'You are now on :version, and everything came back up. Thank you for continuing to trust ProjectSend with your file sharing.',
{ version },
)
: t('What each release brought to this installation.')}
</p>
</div>
{/* The invitation, above the notes on purpose: it is the
part with a person on the other end of it. */}
<DiscordInvitation />
{releases.length > 0 && (
<div className="space-y-8">
<h2 className="text-lg font-semibold tracking-tight">{t("What's new")}</h2>
{releases.map((release) => (
<ReleaseNotes key={release.version} release={release} />
))}
</div>
)}
<div className="space-y-6">
{/* Outline, so the invitation above is the one filled
button on the page. Leaving is not the thing being
encouraged here. */}
<div className="flex justify-center">
<Button asChild variant="outline">
<Link href={route('dashboard')}>
{t('Continue to the dashboard')}
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
<MadeInArgentina />
</div>
</div>
</div>
</AppLayout>
);
}
function ReleaseNotes({ release }: { release: Release }) {
return (
<section className="space-y-4">
<div className="flex items-baseline gap-3 border-b pb-2">
<h3 className="font-semibold">{release.version}</h3>
{release.date !== '' && <span className="text-muted-foreground text-sm">{release.date}</span>}
</div>
{release.intro.map((paragraph, index) => (
<p key={index} className="text-muted-foreground text-sm">
{paragraph}
</p>
))}
{release.groups.map((group, index) => (
<div key={index} className="space-y-3">
{group.heading !== '' && <h4 className="text-sm font-medium">{group.heading}</h4>}
<ul className="space-y-3">
{group.items.map((item, itemIndex) => (
<li key={itemIndex} className="text-sm">
{item.title !== '' && <span className="font-medium">{item.title}. </span>}
<span className="text-muted-foreground">{item.body}</span>
</li>
))}
</ul>
</div>
))}
</section>
);
}
+19 -1
View File
@@ -72,9 +72,12 @@ export interface SharedData {
edition: Edition;
version: string;
links: {
/** projectsend.org, or projectsend.cloud on a managed installation. */
website: string;
source: string;
open_collective: string;
discord: string;
/** Absent on a managed installation — those customers already pay. */
open_collective?: string;
};
/**
* Whether this installation names ProjectSend to its clients and
@@ -107,6 +110,21 @@ export interface SharedData {
/** Decides which upgrade instructions the dialog prints. */
install_kind: InstallKind;
} | null;
/**
* Set when the code this process is running is not the code the
* installation was last updated to — see RunningCodeState. Almost
* always a PHP-FPM that was never reloaded after an update, which is
* otherwise completely silent.
*/
code_notice: {
reason: 'stale_code' | 'pending_update';
/** What the last update applied. */
applied: string;
/** What this process compiled. */
running: string;
applied_at: string;
install_kind: InstallKind;
} | null;
locale: string;
/**
* The IANA zone this viewer reads dates in — their own preference, or
+6 -1
View File
@@ -16,7 +16,12 @@
<meta name="generator" content="ProjectSend">
@endif
<title inertia>{{ config('app.name', 'ProjectSend') }}</title>
{{-- The same name app.tsx suffixes every page title with, from the
same place: the site name in the shared props. Taking it from
APP_NAME instead would show one name in the tab until Inertia
hydrates and a different one after, on any installation whose
administrator renamed the site. --}}
<title inertia>{{ $page['props']['name'] ?? config('app.name', 'ProjectSend') }}</title>
{{-- Which cookie holds this installation's CSRF token. Named after
the installation rather than the framework, so a neighbouring
+2 -1
View File
@@ -19,7 +19,8 @@
<td class="content-cell" align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
@if ($mail_attribution ?? true)
<p><a href="{{ config('projectsend.links.website') }}">{{ __('Powered by ProjectSend') }}</a></p>
{{-- Each edition has its own front door, so this is resolved rather than read straight out of config — see OfficialLinks. --}}
<p><a href="{{ app(App\Modules\Platform\OfficialLinks::class)->website() }}">{{ __('Powered by ProjectSend') }}</a></p>
@endif
</td>
</tr>
+1 -1
View File
@@ -5,5 +5,5 @@
{{ $slot }}
@if ($mail_attribution ?? true)
{{ __('Powered by ProjectSend') }}: {{ config('projectsend.links.website') }}
{{ __('Powered by ProjectSend') }}: {{ app(App\Modules\Platform\OfficialLinks::class)->website() }}
@endif
+2
View File
@@ -16,3 +16,5 @@ Schedule::command('projectsend:fetch-news')->daily();
Schedule::command('projectsend:purge-expired-files')->daily();
Schedule::command('projectsend:purge-orphan-files')->daily();
Schedule::command('projectsend:purge-api-request-logs')->daily();
Schedule::command('projectsend:purge-failed-jobs')->daily();
Schedule::command('projectsend:purge-notifications')->daily();
+27
View File
@@ -19,12 +19,14 @@ use App\Modules\Platform\Http\Controllers\CaptchaSettingsController;
use App\Modules\Platform\Http\Controllers\EmailSettingsController;
use App\Modules\Platform\Http\Controllers\EmailTemplatesController;
use App\Modules\Platform\Http\Controllers\ExternalStorageSettingsController;
use App\Modules\Platform\Http\Controllers\GettingStartedController;
use App\Modules\Platform\Http\Controllers\LanguageSettingsController;
use App\Modules\Platform\Http\Controllers\PrivacySettingsController;
use App\Modules\Platform\Http\Controllers\PublicListingSettingsController;
use App\Modules\Platform\Http\Controllers\SchedulerMonitoringController;
use App\Modules\Platform\Http\Controllers\SystemSettingsController;
use App\Modules\Platform\Http\Controllers\ThemingSettingsController;
use App\Modules\Platform\Http\Controllers\WhatsNewController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function () {
@@ -110,6 +112,22 @@ Route::middleware('auth')->group(function () {
// edit.
Route::middleware('staff')->get('system/about', AboutController::class)->name('system.about');
// Where a new installation's administrator is sent on their first
// visit, and a page any staff member can come back to. `staff` alone:
// it is a list of links to screens they can already reach, and
// QuickStart filters it to the ones they may actually use.
Route::middleware('staff')
->get('system/getting-started', GettingStartedController::class)
->name('system.getting-started');
// Where an administrator is sent after an update, and a page anyone
// who may read About's environment block can revisit afterwards. The
// capability keeps it off managed installations, where nobody signed
// in here performed the update it thanks them for.
Route::middleware(['staff', 'capability:system.updates', 'can:view_system_info'])
->get('system/whats-new', WhatsNewController::class)
->name('system.whats-new');
// System-wide configuration — deliberately outside the account
// settings section; it lives under its own sidebar entry, one route
// per section (general today; clients, email, … as modules land).
@@ -117,6 +135,14 @@ Route::middleware('auth')->group(function () {
Route::redirect('system/settings', '/system/settings/general');
Route::get('system/settings/general', [SystemSettingsController::class, 'edit'])->name('system-settings.edit');
Route::patch('system/settings/general', [SystemSettingsController::class, 'update'])->name('system-settings.update');
// Its own throttle bucket, like every other action route here: a
// bare `throttle:5,1` is keyed on the domain and the address, so
// it would share one allowance with everything else that omits a
// name. The controller enforces manage_updates and the edition,
// and applies an installation-wide cooldown of its own.
Route::post('system/settings/check-for-updates', [SystemSettingsController::class, 'checkForUpdates'])
->middleware('throttle:5,1,check-for-updates-now')
->name('system-settings.check-for-updates');
Route::get('system/settings/security', [SecuritySettingsController::class, 'edit'])->name('system-settings.security.edit');
Route::patch('system/settings/security', [SecuritySettingsController::class, 'update'])->name('system-settings.security.update');
Route::get('system/settings/clients', [ClientSettingsController::class, 'edit'])->name('system-settings.clients.edit');
@@ -177,6 +203,7 @@ Route::middleware('auth')->group(function () {
Route::post('system/settings/scheduler/failed-jobs/{uuid}/retry', [SchedulerMonitoringController::class, 'retryFailedJob'])->name('system-settings.scheduler.retry');
Route::delete('system/settings/scheduler/failed-jobs/{uuid}', [SchedulerMonitoringController::class, 'destroyFailedJob'])->name('system-settings.scheduler.destroy');
Route::delete('system/settings/scheduler/failed-jobs', [SchedulerMonitoringController::class, 'destroyAllFailedJobs'])->name('system-settings.scheduler.destroy-all');
Route::patch('system/settings/scheduler/retention', [SchedulerMonitoringController::class, 'updateRetention'])->name('system-settings.scheduler.retention');
});
});
+6 -1
View File
@@ -1,5 +1,6 @@
<?php
use App\Http\Middleware\RedirectToGreeting;
use App\Modules\Api\Http\Controllers\ApiDashboardController;
use App\Modules\Api\Http\Controllers\ApiDocsController;
use App\Modules\Audit\Http\Controllers\ActivityLogController;
@@ -73,7 +74,11 @@ Route::get('s/{token}', [PublicShareController::class, 'show'])->middleware('thr
Route::get('s/{token}/download', [PublicShareController::class, 'download'])->middleware('throttle:30,1,share-link')->name('share.download');
Route::middleware(['auth'])->group(function () {
Route::get('dashboard', DashboardController::class)->name('dashboard');
// The greeting middleware sits here and nowhere else — see the class
// for why the dashboard is the right and only place to intercept.
Route::get('dashboard', DashboardController::class)
->middleware(RedirectToGreeting::class)
->name('dashboard');
Route::put('dashboard/widgets', [DashboardWidgetPreferencesController::class, 'update'])->name('dashboard.widgets.update');
// Every account (staff or client) has their own notifications —
+20
View File
@@ -197,6 +197,26 @@ test('folder ownership splits own from others for edit and delete', function ()
$this->delete("/folders/{$others->id}")->assertForbidden();
});
// Issue #1645, end to end through the screen the report came from: a deleted
// folder used to keep its public URL, so the name could never be used again
// and the error named a row the interface will not show.
test('a deleted folder gives its name and its public URL back', function () {
$this->actingAs($this->admin);
$this->post('/folders', ['name' => 'Quarterly', 'public' => true, 'slug' => 'quarterly'])
->assertRedirect()->assertSessionHasNoErrors();
$first = Folder::query()->where('name', 'Quarterly')->sole();
$this->delete("/folders/{$first->id}")->assertRedirect();
$this->post('/folders', ['name' => 'Quarterly', 'public' => true, 'slug' => 'quarterly'])
->assertRedirect()->assertSessionHasNoErrors();
$second = Folder::query()->where('name', 'Quarterly')->sole();
expect($second->id)->not->toBe($first->id)
->and($second->slug)->toBe('quarterly');
});
test('creating folders requires create_own_folders', function () {
// Account Manager lacks create_own_folders in the v1 default set.
$manager = User::factory()->role(SystemRole::AccountManager)->create();
+87 -2
View File
@@ -6,7 +6,11 @@ use App\Models\User;
use App\Modules\Files\Models\File;
use App\Modules\Files\Models\Folder;
use App\Modules\Groups\Models\Group;
use App\Support\Rules;
use App\Support\VacatedSlug;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
beforeEach(function () {
Storage::fake('files');
@@ -34,12 +38,93 @@ test('colliding names get a numeric suffix that keeps counting up', function ()
->and(File::factory()->create(['name' => 'Report'])->slug)->toBe('report-3');
});
test('a soft-deleted row still collides, so its URL is not silently reused', function () {
// Deleting used to hold the slug forever, which burned the name: nothing can
// restore a trashed row, so it was reserved for a page that could never come
// back. See issue #1645.
test('deleting a row hands its slug back', function () {
$first = File::factory()->create(['name' => 'Report']);
$first->delete();
expect($first->trashed())->toBeTrue()
->and(File::factory()->create(['name' => 'Report'])->slug)->toBe('report-2');
->and(File::factory()->create(['name' => 'Report'])->slug)->toBe('report');
});
test('the vacated slug is parked where nothing can collide with it', function () {
$file = File::factory()->create(['name' => 'Report']);
$file->delete();
$parked = File::withTrashed()->whereKey($file->id)->value('slug');
expect($parked)->toBe('report'.VacatedSlug::MARKER.$file->id)
// Neither route into a slug can produce that string, which is what
// makes parking there safe rather than merely unlikely.
->and(Str::slug($parked))->not->toContain('_')
->and(Validator::make(
['slug' => $parked, 'public' => true],
['slug' => Rules::slug('files')],
)->fails())->toBeTrue();
});
test('the name is reusable however many times it is deleted', function () {
foreach (range(1, 3) as $ignored) {
$file = File::factory()->create(['name' => 'Report']);
expect($file->slug)->toBe('report');
$file->delete();
}
// Three deleted rows, each parked under its own id rather than piling up
// as report-2, report-3, report-4 the way the suffix used to.
expect(File::withTrashed()->where('slug', 'like', 'report'.VacatedSlug::MARKER.'%')->count())->toBe(3);
});
test('all three models hand the slug back', function () {
$file = File::factory()->create(['name' => 'Shared']);
$folder = Folder::query()->create(['name' => 'Shared', 'path' => '/']);
$group = Group::query()->create(['name' => 'Shared']);
$file->delete();
$folder->delete();
$group->delete();
expect(File::factory()->create(['name' => 'Shared'])->slug)->toBe('shared')
->and(Folder::query()->create(['name' => 'Shared', 'path' => '/'])->slug)->toBe('shared')
->and(Group::query()->create(['name' => 'Shared'])->slug)->toBe('shared');
});
// The reported bug: a public folder's slug is user-supplied and validated
// against the table, so a trashed row holding it made the name unusable with
// an error naming a row the interface will not show.
test('a deleted public row does not block its slug at the validator', function () {
$folder = Folder::query()->create(['name' => 'Quarterly', 'path' => '/', 'public' => true, 'slug' => 'quarterly']);
$folder->delete();
expect(Validator::make(
['slug' => 'quarterly', 'public' => true],
['slug' => Rules::slug('folders')],
)->fails())->toBeFalse();
});
test('force-deleting leaves nothing behind to vacate', function () {
$file = File::factory()->create(['name' => 'Report']);
$file->forceDelete();
expect(File::withTrashed()->whereKey($file->id)->exists())->toBeFalse()
->and(File::factory()->create(['name' => 'Report'])->slug)->toBe('report');
});
test('vacating does not look like somebody edited the row', function () {
$file = File::factory()->create(['name' => 'Report']);
$this->travel(5)->minutes();
$file->delete();
// The soft delete moves updated_at itself; what matters is that vacating
// the slug afterwards is not a second write on top of it, so the two
// stamps the delete wrote still agree.
$row = File::withTrashed()->whereKey($file->id)->sole();
expect($row->slug)->toBe('report'.VacatedSlug::MARKER.$file->id)
->and($row->updated_at)->toEqual($row->deleted_at);
});
test('a name that slugs to nothing falls back to a per-model default', function () {
+37
View File
@@ -6,6 +6,8 @@ use App\Models\User;
use App\Modules\Platform\Attribution\Attribution;
use App\Modules\Platform\Attribution\Events\ResolvingAttribution;
use App\Modules\Platform\Capabilities\Edition;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Support\Facades\Event;
use Inertia\Testing\AssertableInertia;
@@ -91,3 +93,38 @@ test('the generator meta names ProjectSend without its version', function () {
// who asks, and it would tell a scanner which advisories apply.
$response->assertDontSee('content="ProjectSend '.config('projectsend.version').'"', false);
});
test('about names the version this installation was last updated to, and when', function () {
config()->set('projectsend.edition', Edition::Community);
$settings = app(Settings::class);
$settings->set(Setting::AppliedVersion, '2.1.0');
$settings->set(Setting::AppliedVersionAt, '2026-08-17T10:00:00+00:00');
$this->actingAs($this->admin)->get('/system/about')->assertInertia(
fn (AssertableInertia $page) => $page
->where('updated.version', '2.1.0')
->where('updated.at', '2026-08-17T10:00:00+00:00'),
);
});
test('an installation that has never been updated says nothing rather than guessing', function () {
config()->set('projectsend.edition', Edition::Community);
$settings = app(Settings::class);
$settings->set(Setting::AppliedVersion, '');
$settings->set(Setting::AppliedVersionAt, '');
$this->actingAs($this->admin)->get('/system/about')->assertInertia(
fn (AssertableInertia $page) => $page->where('updated', null),
);
});
test('the last-update line is hidden from anyone the environment block is hidden from', function () {
config()->set('projectsend.edition', Edition::Cloud);
$settings = app(Settings::class);
$settings->set(Setting::AppliedVersion, '2.1.0');
$settings->set(Setting::AppliedVersionAt, '2026-08-17T10:00:00+00:00');
$this->actingAs($this->admin)->get('/system/about')->assertInertia(
fn (AssertableInertia $page) => $page->where('environment', null)->where('updated', null),
);
});
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Identity\Models\Role;
use App\Modules\Identity\Models\RolePermission;
use App\Modules\Platform\Capabilities\Edition;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Support\Facades\Http;
use Inertia\Testing\AssertableInertia;
/**
* The "check now" button in the general settings. The check itself is
* covered by CheckForUpdatesCommandTest; what is tested here is who may
* press it, and that pressing it repeatedly cannot spend the
* installation's whole allowance with the release feed.
*
* @param array<string, mixed> $extra
*/
function fakeReleaseFeed(string $tagName, array $extra = []): void
{
Http::fake([
'api.github.com/repos/projectsend/projectsend/releases/latest' => Http::response([
'tag_name' => $tagName,
'name' => $extra['name'] ?? "Release {$tagName}",
'body' => $extra['body'] ?? 'Some release notes.',
'html_url' => $extra['html_url'] ?? "https://github.com/projectsend/projectsend/releases/tag/{$tagName}",
'published_at' => $extra['published_at'] ?? '2026-08-02T00:00:00Z',
], 200),
]);
}
beforeEach(function () {
config()->set('projectsend.version', '2.0.0');
config()->set('projectsend.edition', Edition::Community);
// Both are cached beyond the per-test rollback, so every test states
// what it needs rather than inheriting whatever ran before it.
$settings = app(Settings::class);
$settings->set(Setting::CheckForUpdates, true);
$settings->set(Setting::LatestVersionCheckedAt, '');
$settings->set(Setting::LatestKnownVersion, '');
});
test('an administrator can ask the feed on demand and is told what it said', function () {
$this->actingAs(User::factory()->create());
fakeReleaseFeed('v2.5.0');
$this->post('/system/settings/check-for-updates')
->assertRedirect()
->assertSessionHas('update_check_result', fn (array $result): bool => $result['ok'] === true
&& str_contains($result['message'], '2.5.0'));
expect(app(Settings::class)->get(Setting::LatestKnownVersion))->toBe('2.5.0');
});
test('the answer reaches the page that asked for it', function () {
$this->actingAs(User::factory()->create());
fakeReleaseFeed('v2.5.0');
$this->post('/system/settings/check-for-updates');
$this->get('/system/settings/general')->assertInertia(
fn (AssertableInertia $page) => $page
->component('system/settings/general')
->where('check_result.ok', true)
->has('last_checked_at'),
);
});
test('a second press within the cooldown re-reads the answer instead of the feed', function () {
$this->actingAs(User::factory()->create());
fakeReleaseFeed('v2.5.0');
$this->post('/system/settings/check-for-updates');
$this->post('/system/settings/check-for-updates')->assertRedirect();
Http::assertSentCount(1);
});
test('the daily check being switched off does not refuse a question somebody asked out loud', function () {
app(Settings::class)->set(Setting::CheckForUpdates, false);
$this->actingAs(User::factory()->create());
fakeReleaseFeed('v2.5.0');
$this->post('/system/settings/check-for-updates');
Http::assertSentCount(1);
expect(app(Settings::class)->get(Setting::LatestKnownVersion))->toBe('2.5.0');
});
test('an unreachable feed says so and leaves every cached answer alone', function () {
app(Settings::class)->set(Setting::LatestKnownVersion, '2.4.0');
$this->actingAs(User::factory()->create());
Http::fake(['api.github.com/*' => Http::response(null, 500)]);
$this->post('/system/settings/check-for-updates')
->assertSessionHas('update_check_result', fn (array $result): bool => $result['ok'] === false);
expect(app(Settings::class)->get(Setting::LatestKnownVersion))->toBe('2.4.0');
});
test('a staff member without manage_updates cannot press it', function () {
$role = Role::query()->create(['name' => 'Settings Only', 'is_administrator' => false, 'is_system' => false]);
RolePermission::query()->insert(['role_id' => $role->id, 'permission' => 'edit_settings']);
$this->actingAs(User::factory()->create(['role_id' => $role->id]));
fakeReleaseFeed('v2.5.0');
$this->post('/system/settings/check-for-updates')->assertForbidden();
Http::assertNothingSent();
});
test('the cloud edition has nothing to check', function () {
config()->set('projectsend.edition', Edition::Cloud);
$this->actingAs(User::factory()->create());
fakeReleaseFeed('v2.5.0');
$this->post('/system/settings/check-for-updates')->assertForbidden();
Http::assertNothingSent();
});
@@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Identity\Permissions\SystemRole;
use App\Modules\Platform\Capabilities\Edition;
use App\Modules\Platform\Scheduling\ScheduledTaskRun;
use App\Modules\Platform\Scheduling\TaskRunStatus;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Inertia\Testing\AssertableInertia;
/**
* A new installation shows its administrator around, once — and the list
* it shows never points at something this edition or this person cannot
* do.
*/
beforeEach(function () {
$this->admin = User::factory()->create();
// Settings survive RefreshDatabase's rollback in the cache, so state
// both markers rather than assuming their defaults.
app(Settings::class)->set(Setting::GettingStartedPending, false);
app(Settings::class)->set(Setting::UpdateWelcomeTo, '');
});
function justInstalled(): void
{
app(Settings::class)->set(Setting::GettingStartedPending, true);
}
/** @return list<string> */
function quickStartKeys(User $user): array
{
$keys = [];
test()->actingAs($user)->get('/system/getting-started')->assertInertia(
function (AssertableInertia $page) use (&$keys) {
$keys = array_column($page->toArray()['props']['items'], 'key');
},
);
return $keys;
}
test('the main administrator lands on it after installing', function () {
justInstalled();
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
});
test('it happens exactly once', function () {
justInstalled();
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
$this->actingAs($this->admin)->get('/system/getting-started')->assertOk();
$this->actingAs($this->admin)->get('/dashboard')->assertOk();
});
// Closing it on the way past should not be unrecoverable.
test('it stays readable afterwards, with the welcome wording dropped', function () {
justInstalled();
$this->actingAs($this->admin)->get('/system/getting-started')->assertOk();
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(
fn (AssertableInertia $page) => $page
->component('system/getting-started')
->where('justInstalled', false)
->has('items'),
);
});
test('other staff are not interrupted, but may read it', function () {
justInstalled();
$second = User::factory()->create();
$this->actingAs($second)->get('/dashboard')->assertOk();
$this->actingAs($second)->get('/system/getting-started')->assertOk();
// …and reading it did not consume the greeting.
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
});
test('clients cannot reach it', function () {
$client = User::factory()->client()->create();
// EnsureStaff redirects a client away from a staff GET rather than
// answering 403 — see its docblock.
$this->actingAs($client)->get('/system/getting-started')->assertRedirect();
});
test('an installation that merely updated is never welcomed to itself', function () {
app(Settings::class)->set(Setting::UpdateWelcomeFrom, '2.0.0');
app(Settings::class)->set(Setting::UpdateWelcomeTo, '2.1.0');
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/whats-new');
});
// Both markers at once cannot happen in practice — an update marker is
// only raised for an installation that already existed — but if it did,
// release notes for a version they never ran are the wrong greeting.
test('installing wins over updating', function () {
justInstalled();
app(Settings::class)->set(Setting::UpdateWelcomeTo, '2.1.0');
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
});
test('completing setup raises the greeting', function () {
User::query()->delete();
$this->post('/setup', [
'site_name' => 'Acme Files',
'name' => 'Ada',
'email' => 'ada@example.com',
'password' => 'a-long-enough-password',
'password_confirmation' => 'a-long-enough-password',
])->assertRedirect('/setup/success');
expect(app(Settings::class)->get(Setting::GettingStartedPending))->toBeTrue();
});
// Unattended provisioning skips the setup screen entirely, and is how
// every container that came up from environment variables was installed.
test('provisioning from the command line raises it too', function () {
User::query()->delete();
$this->artisan('projectsend:admin', [
'--name' => 'Ada',
'--email' => 'ada@example.com',
'--password' => 'a-long-enough-password',
])->assertSuccessful();
expect(app(Settings::class)->get(Setting::GettingStartedPending))->toBeTrue();
});
test('--if-none on an installed site raises nothing', function () {
$this->artisan('projectsend:admin', ['--if-none' => true])->assertSuccessful();
expect(app(Settings::class)->get(Setting::GettingStartedPending))->toBeFalse();
});
// The list is the point of the page, and a link to a screen that answers
// 403 is worse than no link at all.
test('it only lists what this person may actually do', function () {
$uploader = User::factory()->role(SystemRole::Uploader)->create();
$keys = quickStartKeys($uploader);
expect($keys)->toContain('upload')
->and($keys)->not->toContain('team')
->and($keys)->not->toContain('email')
->and($keys)->not->toContain('theme');
});
// The example the brief named: a managed installation has no staff
// accounts of its own to hand out, no mail server to point anywhere and
// no scheduler to check.
test('a managed installation is not sent to screens it does not have', function () {
config()->set('projectsend.edition', Edition::Cloud);
$keys = quickStartKeys($this->admin);
expect($keys)->toContain('client', 'upload', 'theme', 'email-theme')
->and($keys)->not->toContain('team')
->and($keys)->not->toContain('email')
->and($keys)->not->toContain('scheduler');
});
test('a self-hosted installation gets the full list', function () {
config()->set('projectsend.edition', Edition::Community);
expect(quickStartKeys($this->admin))->toContain('client', 'upload', 'group', 'theme', 'email-theme', 'email', 'team', 'scheduler');
});
// Two of them can be answered from the database rather than guessed, and
// a tick that means "we assume so" would be worse than no tick.
test('the client and upload steps tick themselves', function () {
justInstalled();
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(
fn (AssertableInertia $page) => $page->where('items.0.key', 'client')->where('items.0.done', false),
);
User::factory()->client()->create();
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(
fn (AssertableInertia $page) => $page->where('items.0.done', true),
);
});
// Not every step is equally urgent, and a list that pretends otherwise is
// a list nobody reads twice. The mail server is the one whose absence is
// silent — nothing announces that password resets are going nowhere.
test('it marks which steps the installation does not work without', function () {
$items = [];
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(function (AssertableInertia $page) use (&$items) {
$items = collect($page->toArray()['props']['items'])->keyBy('key');
});
expect($items['email']['essential'])->toBeTrue()
->and($items['client']['essential'])->toBeTrue()
->and($items['scheduler']['essential'])->toBeTrue()
->and($items['email-theme']['essential'])->toBeFalse()
->and($items['team']['essential'])->toBeFalse();
});
// The email-theme link has to land on the email tab, not on the page with
// a sentence asking the reader to find it.
test('the email theme step deep-links to its own tab', function () {
$items = [];
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(function (AssertableInertia $page) use (&$items) {
$items = $page->toArray()['props']['items'];
});
$emailTheme = collect($items)->firstWhere('key', 'email-theme');
expect($emailTheme['href'])->toContain('tab=email');
});
// The one essential step that could never be completed: it was hardcoded
// unticked, while the screen it links to was already answering the
// question from these very rows.
test('the scheduler step ticks itself once the scheduler has run here', function () {
$items = [];
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(function (AssertableInertia $page) use (&$items) {
$items = collect($page->toArray()['props']['items'])->keyBy('key');
});
expect($items['scheduler']['done'])->toBeFalse();
ScheduledTaskRun::query()->create([
'command' => 'projectsend:purge-expired-files',
'status' => TaskRunStatus::Success,
'message' => null,
'duration_ms' => 12,
'ran_at' => now(),
]);
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(function (AssertableInertia $page) use (&$items) {
$items = collect($page->toArray()['props']['items'])->keyBy('key');
});
expect($items['scheduler']['done'])->toBeTrue();
});
// A run that failed still proves cron reaches this installation, which is
// what the step asks. Why it failed is the Scheduler screen's job.
test('a failed run still counts as the scheduler running', function () {
ScheduledTaskRun::query()->create([
'command' => 'projectsend:fetch-news',
'status' => TaskRunStatus::Failed,
'message' => 'Scheduled command failed with exit code [1].',
'duration_ms' => null,
'ran_at' => now(),
]);
$items = [];
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(function (AssertableInertia $page) use (&$items) {
$items = collect($page->toArray()['props']['items'])->keyBy('key');
});
expect($items['scheduler']['done'])->toBeTrue();
});
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Platform\Capabilities\Edition;
use App\Modules\Platform\OfficialLinks;
use Inertia\Testing\AssertableInertia;
/**
* Each edition has its own front door, and only one of them asks for
* donations.
*/
beforeEach(function () {
$this->admin = User::factory()->create();
});
function officialLinks(Edition $edition): array
{
config()->set('projectsend.edition', $edition);
app()->forgetInstance(App\Modules\Platform\Capabilities\CapabilityRegistry::class);
return app(OfficialLinks::class)->toArray();
}
test('a self-hosted installation points at projectsend.org', function () {
$links = officialLinks(Edition::Community);
expect($links['website'])->toBe('https://www.projectsend.org/');
});
test('a managed installation points at projectsend.cloud', function () {
$links = officialLinks(Edition::Cloud);
expect($links['website'])->toBe('https://www.projectsend.cloud/');
});
// Omitted rather than hidden by the page: a surface added later cannot
// ask a paying customer for a donation by forgetting to check.
test('a managed installation offers no donation link at all', function () {
expect(officialLinks(Edition::Cloud))->not->toHaveKey('open_collective')
->and(officialLinks(Edition::Community))->toHaveKey('open_collective');
});
test('the source and the community are the same for both', function () {
$community = officialLinks(Edition::Community);
$cloud = officialLinks(Edition::Cloud);
expect($cloud['source'])->toBe($community['source'])
->and($cloud['discord'])->toBe($community['discord']);
});
test('the resolved links are what every page is handed', function () {
config()->set('projectsend.edition', Edition::Cloud);
app()->forgetInstance(App\Modules\Platform\Capabilities\CapabilityRegistry::class);
$this->actingAs($this->admin)->get('/system/about')->assertInertia(
fn (AssertableInertia $page) => $page
->where('links.website', 'https://www.projectsend.cloud/')
->missing('links.open_collective'),
);
});
// The "Powered by ProjectSend" line at the foot of every outgoing message
// is where a recipient meets the product for the first time. Sending a
// hosted customer's recipients to the self-hosting instructions is the
// wrong door, and that footer reads the link straight out of Blade.
test('the mail footers resolve the link rather than reading the config', function () {
foreach (['html', 'text'] as $format) {
$footer = file_get_contents(resource_path("views/vendor/mail/{$format}/footer.blade.php"));
expect($footer)->toContain('OfficialLinks')
->and($footer)->not->toContain("config('projectsend.links.website')");
}
});
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Support\Facades\File;
/**
* A companion package owns whole screens — Branding, Custom Assets, the
* v1 import — and every string on them was rendering in English in all
* sixteen languages, because the frontend read lang/{locale}.json
* directly and a package's catalogue is somewhere else entirely.
*/
beforeEach(function () {
// SetLocale honours the account's own language only while that
// language is switched on for the installation, and the Settings cache
// outlives RefreshDatabase — so say it rather than assume it.
app(Settings::class)->set(Setting::EnabledLocales, ['en', 'es']);
$this->admin = User::factory()->create(['locale' => 'es']);
$this->packageLang = sys_get_temp_dir().'/package-lang-'.bin2hex(random_bytes(6));
File::makeDirectory($this->packageLang);
});
afterEach(function () {
File::deleteDirectory($this->packageLang);
});
function packageCatalogue(string $dir, array $entries): void
{
File::put($dir.'/es.json', json_encode($entries, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
app('translator')->getLoader()->addJsonPath($dir);
}
function sharedTranslations(User $user): array
{
return test()->actingAs($user)->get('/dashboard')->viewData('page')['props']['translations'];
}
test('a package catalogue reaches the frontend', function () {
packageCatalogue($this->packageLang, ['Attribution settings saved.' => 'Ajustes de atribución guardados.']);
expect(sharedTranslations($this->admin))
->toHaveKey('Attribution settings saved.', 'Ajustes de atribución guardados.');
});
test('the application keeps its own strings', function () {
packageCatalogue($this->packageLang, ['Something from a package' => 'Algo de un paquete']);
$translations = sharedTranslations($this->admin);
expect($translations)->toHaveKey('Something from a package')
// Any string this repository owns, still translated as before.
->and($translations['Dashboard'] ?? null)->toBe('Panel de control');
});
// The useful way round: an installation can override a package's wording
// in its own catalogue without editing the package.
test('the installation wins when both define the same key', function () {
packageCatalogue($this->packageLang, ['Dashboard' => 'From the package']);
expect(sharedTranslations($this->admin)['Dashboard'])->toBe('Panel de control');
});
// English is the key, so there is nothing to send and nothing to merge.
test('english still ships no catalogue at all', function () {
packageCatalogue($this->packageLang, ['Attribution' => 'Should never be sent']);
$english = User::factory()->create(['locale' => 'en']);
expect(sharedTranslations($english))->toBe([]);
});
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
use App\Modules\Platform\Updates\ReleaseNotes;
/**
* The changelog is written for people, by hand, and this reads it. Every
* shape asserted here is one that exists in CHANGELOG.md today — the
* parser is not general-purpose Markdown, and does not need to be.
*/
function changelog(): string
{
return <<<'MARKDOWN'
# Changelog
What changed in each release, written for the people running it.
## Unreleased
Collected as it lands.
## 2.2.0 — 2026-09-01
A short release.
### Fixed
- **Downloads no longer stall.** The part that hung is gone, and the retry
is now bounded.
## 2.1.0 — 2026-08-20
### Added
- **A REST API.** Files and clients, with [scoped tokens](https://example.com) and `php artisan` docs.
- **Clients sign in with their email address**, not their username.
### Upgrade notes
- Run the migrations before starting the workers.
## 2.0.0 — 2026-08-14
The rewrite.
MARKDOWN;
}
it('reads a release into its heading, date and items', function (): void {
$releases = (new ReleaseNotes)->all(changelog());
expect($releases)->toHaveCount(3)
->and($releases[0]['version'])->toBe('2.2.0')
->and($releases[0]['date'])->toBe('2026-09-01')
->and($releases[0]['intro'])->toBe(['A short release.'])
->and($releases[0]['groups'][0]['heading'])->toBe('Fixed')
->and($releases[0]['groups'][0]['items'][0]['title'])->toBe('Downloads no longer stall');
});
// "Unreleased" carries no version number, so it never matches the heading
// pattern — which is the whole reason the pattern requires one.
it('skips the unreleased section', function (): void {
$versions = array_column((new ReleaseNotes)->all(changelog()), 'version');
expect($versions)->toBe(['2.2.0', '2.1.0', '2.0.0']);
});
// Bullets wrap at 100 columns in the file; a reader wants the sentence.
it('joins a bullet that wraps across lines', function (): void {
$item = (new ReleaseNotes)->all(changelog())[0]['groups'][0]['items'][0];
expect($item['body'])->toBe('The part that hung is gone, and the retry is now bounded.');
});
it('keeps link text and drops the markup around it', function (): void {
$item = (new ReleaseNotes)->all(changelog())[1]['groups'][0]['items'][0];
expect($item['title'])->toBe('A REST API')
->and($item['body'])->toBe('Files and clients, with scoped tokens and php artisan docs.');
});
// A bold fragment the sentence continues through is not a lead-in.
// Promoting it produced "Clients sign in with their email address. , not
// their username" on the page.
it('leaves a bullet whose bold opening is not a sentence intact', function (): void {
$item = (new ReleaseNotes)->all(changelog())[1]['groups'][0]['items'][1];
expect($item['title'])->toBe('')
->and($item['body'])->toBe('Clients sign in with their email address, not their username.');
});
it('keeps a bullet with no bold at all', function (): void {
$group = (new ReleaseNotes)->all(changelog())[1]['groups'][1];
expect($group['heading'])->toBe('Upgrade notes')
->and($group['items'][0])->toBe(['title' => '', 'body' => 'Run the migrations before starting the workers.']);
});
// The point of recording both versions: somebody who updates twice a year
// crosses several releases at once, and all of them are news to them.
it('returns every release in the gap, newest first', function (): void {
$versions = array_column((new ReleaseNotes)->between('2.0.0', '2.2.0', changelog()), 'version');
expect($versions)->toBe(['2.2.0', '2.1.0']);
});
it('excludes the version being updated from, and anything newer than the one reached', function (): void {
$versions = array_column((new ReleaseNotes)->between('2.1.0', '2.1.0', changelog()), 'version');
expect($versions)->toBe([]);
});
// The first update of an installation older than this feature has no
// recorded previous version. One release is better than the entire history.
it('shows only the version reached when the previous one is unknown', function (): void {
$versions = array_column((new ReleaseNotes)->between('', '2.1.0', changelog()), 'version');
expect($versions)->toBe(['2.1.0']);
});
// A release whose notes were never written up: the page drops the section
// rather than inventing one.
it('returns nothing for a version the changelog does not mention', function (): void {
expect((new ReleaseNotes)->between('2.2.0', '2.3.0', changelog()))->toBe([]);
});
// The real file, so a change to how the changelog is written shows up
// here rather than on an administrator's screen.
it('reads the changelog this release actually ships', function (): void {
$releases = (new ReleaseNotes)->between('', (string) config('projectsend.version'));
expect($releases)->not->toBeEmpty()
->and($releases[0]['version'])->toBe(config('projectsend.version'))
->and($releases[0]['groups'])->not->toBeEmpty();
});
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Notifications\InAppNotification;
use App\Modules\Platform\Capabilities\Edition;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
/**
* The two tables that grow on their own — failed queue jobs and read
* notifications — and the daily purges that now keep them from growing
* forever on an installation nobody tends.
*/
beforeEach(function () {
config()->set('projectsend.edition', Edition::Community);
$this->admin = User::factory()->create();
// Settings outlive the per-test rollback in the cache, so both
// windows are stated rather than assumed.
$settings = app(Settings::class);
$settings->set(Setting::FailedJobRetentionDays, 30);
$settings->set(Setting::NotificationRetentionDays, 90);
});
function insertFailedJob(string $failedAt): string
{
$uuid = (string) Str::uuid();
DB::table('failed_jobs')->insert([
'uuid' => $uuid,
'connection' => 'redis',
'queue' => 'default',
'payload' => '{}',
'exception' => 'Something went wrong.',
'failed_at' => $failedAt,
]);
return $uuid;
}
test('it deletes failed jobs past the window and keeps the recent ones', function () {
$old = insertFailedJob(now()->subDays(45)->toDateTimeString());
$recent = insertFailedJob(now()->subDays(3)->toDateTimeString());
$this->artisan('projectsend:purge-failed-jobs')->assertSuccessful();
expect(DB::table('failed_jobs')->where('uuid', $old)->exists())->toBeFalse()
->and(DB::table('failed_jobs')->where('uuid', $recent)->exists())->toBeTrue();
});
test('a retention of zero keeps every failed job, however old', function () {
app(Settings::class)->set(Setting::FailedJobRetentionDays, 0);
$old = insertFailedJob(now()->subYears(3)->toDateTimeString());
$this->artisan('projectsend:purge-failed-jobs')->assertSuccessful();
expect(DB::table('failed_jobs')->where('uuid', $old)->exists())->toBeTrue();
});
test('it deletes read notifications past the window', function () {
$old = InAppNotification::query()->create([
'user_id' => $this->admin->id,
'type' => 'file_shared',
'read_at' => now()->subDays(100),
'created_at' => now()->subDays(100),
]);
$recent = InAppNotification::query()->create([
'user_id' => $this->admin->id,
'type' => 'file_shared',
'read_at' => now()->subDay(),
'created_at' => now()->subDay(),
]);
$this->artisan('projectsend:purge-notifications')->assertSuccessful();
expect(InAppNotification::query()->whereKey($old->id)->exists())->toBeFalse()
->and(InAppNotification::query()->whereKey($recent->id)->exists())->toBeTrue();
});
test('an unread notification survives at any age', function () {
$ancient = InAppNotification::query()->create([
'user_id' => $this->admin->id,
'type' => 'file_shared',
'read_at' => null,
'created_at' => now()->subYears(2),
]);
$this->artisan('projectsend:purge-notifications')->assertSuccessful();
expect(InAppNotification::query()->whereKey($ancient->id)->exists())->toBeTrue();
});
test('a retention of zero keeps read notifications too', function () {
app(Settings::class)->set(Setting::NotificationRetentionDays, 0);
$old = InAppNotification::query()->create([
'user_id' => $this->admin->id,
'type' => 'file_shared',
'read_at' => now()->subYears(2),
'created_at' => now()->subYears(2),
]);
$this->artisan('projectsend:purge-notifications')->assertSuccessful();
expect(InAppNotification::query()->whereKey($old->id)->exists())->toBeTrue();
});
test('both windows are set from the scheduler screen', function () {
$this->actingAs($this->admin)->get('/system/settings/scheduler')->assertInertia(
fn (AssertableInertia $page) => $page
->where('retention.failed_jobs', 30)
->where('retention.notifications', 90),
);
$this->actingAs($this->admin)
->patch('/system/settings/scheduler/retention', ['failed_jobs' => 7, 'notifications' => 0])
->assertRedirect();
$settings = app(Settings::class);
expect($settings->get(Setting::FailedJobRetentionDays))->toBe(7)
->and($settings->get(Setting::NotificationRetentionDays))->toBe(0);
});
test('a negative window is refused rather than stored', function () {
$this->actingAs($this->admin)
->patch('/system/settings/scheduler/retention', ['failed_jobs' => -1, 'notifications' => 90])
->assertSessionHasErrors('failed_jobs');
expect(app(Settings::class)->get(Setting::FailedJobRetentionDays))->toBe(30);
});
test('the cloud edition has no scheduler screen to set them on', function () {
config()->set('projectsend.edition', Edition::Cloud);
$this->actingAs($this->admin)
->patch('/system/settings/scheduler/retention', ['failed_jobs' => 7, 'notifications' => 7])
->assertNotFound();
});
@@ -7,6 +7,9 @@ use App\Modules\Identity\Models\Role;
use App\Modules\Platform\Capabilities\Edition;
use App\Modules\Platform\Scheduling\ScheduledTaskRun;
use App\Modules\Platform\Scheduling\TaskRunStatus;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
@@ -42,7 +45,7 @@ test('the scheduler page lists every known command, flagging ones that have neve
]);
$response = $this->actingAs($this->admin)->get('/system/settings/scheduler');
$response->assertInertia(fn (AssertableInertia $page) => $page->component('system/settings/scheduler')->has('tasks', 7));
$response->assertInertia(fn (AssertableInertia $page) => $page->component('system/settings/scheduler')->has('tasks', 10));
$tasks = collect(schedulerPageProps($response)['tasks'])->keyBy('command');
expect($tasks->get('projectsend:purge-expired-files')['status'])->toBe('success')
@@ -167,3 +170,55 @@ test('staff without edit_settings cannot access the scheduler page', function ()
$this->actingAs($staffer)->get('/system/settings/scheduler')->assertForbidden();
});
test('the update check reports what it found, not just that it ran', function () {
config()->set('projectsend.version', '2.0.0');
$settings = app(Settings::class);
$settings->set(Setting::LatestVersionCheckedAt, now()->toIso8601String());
$settings->set(Setting::LatestKnownVersion, '2.5.0');
$response = $this->actingAs($this->admin)->get('/system/settings/scheduler');
$tasks = collect(schedulerPageProps($response)['tasks'])->keyBy('command');
expect($tasks->get('projectsend:check-for-updates')['detail'])->toContain('2.5.0');
});
test('a check that found nothing newer says so', function () {
config()->set('projectsend.version', '2.0.0');
$settings = app(Settings::class);
$settings->set(Setting::LatestVersionCheckedAt, now()->toIso8601String());
$settings->set(Setting::LatestKnownVersion, '2.0.0');
$response = $this->actingAs($this->admin)->get('/system/settings/scheduler');
$tasks = collect(schedulerPageProps($response)['tasks'])->keyBy('command');
expect($tasks->get('projectsend:check-for-updates')['detail'])->toBe('Up to date');
});
test('a check that has never run has nothing to report', function () {
$settings = app(Settings::class);
$settings->set(Setting::LatestVersionCheckedAt, '');
$settings->set(Setting::LatestKnownVersion, '');
$response = $this->actingAs($this->admin)->get('/system/settings/scheduler');
$tasks = collect(schedulerPageProps($response)['tasks'])->keyBy('command');
expect($tasks->get('projectsend:check-for-updates')['detail'])->toBeNull();
});
// The list on the screen and the schedule are two hand-maintained lists of
// the same thing, and they had already drifted once: the API request log
// purge ran nightly without ever appearing here, so a failure of it was
// invisible on the screen built to make failures visible.
test('every scheduled command appears on the screen', function () {
$scheduled = collect(app(Schedule::class)->events())
->map(fn ($event): string => (string) Str::of((string) $event->command)->match('/projectsend:[\w-]+/'))
->filter()
->sort()
->values();
$response = $this->actingAs($this->admin)->get('/system/settings/scheduler');
$listed = collect(schedulerPageProps($response)['tasks'])->pluck('command')->sort()->values();
expect($listed->all())->toBe($scheduled->all());
});

Some files were not shown because too many files have changed in this diff Show More