Compare commits

..

192 Commits

Author SHA1 Message Date
Corentin Thomasset
afdcc1c5ba feat(demo): add subscription and usage endpoints (#559) 2025-10-19 23:18:33 +02:00
Corentin Thomasset
92daaa35bb fix(webhooks): omit secret from webhook response in update route (#566) 2025-10-19 14:04:59 +00:00
Corentin Thomasset
e4295e14ab fix(theme): prevent flash of wrong theme on load (#565) 2025-10-19 15:55:00 +02:00
Corentin Thomasset
ae37d1db36 fix(tasks): add FLY_MACHINE_ID fallback to worker ids (#564) 2025-10-19 12:16:42 +00:00
Corentin Thomasset
a7464f8b89 chore(fly): update configuration for deployment and health checks (#563) 2025-10-18 21:42:09 +00:00
Corentin Thomasset
2dd9ca9835 chore(fly): test fly.io hosting (#561) 2025-10-18 14:17:17 +00:00
Corentin Thomasset
54cc14052c refactor(tracking): replace posthog-js with posthog-js-lite to reduced bundle (#560) 2025-10-17 21:22:56 +00:00
Corentin Thomasset
f930e46dde fix(docker): correct package changelog title to @papra/docker (#551) 2025-10-16 16:15:17 +02:00
Corentin Thomasset
df75e5accb feat(subscriptions): add global coupon support for checkout sessions (#558) 2025-10-16 15:36:42 +02:00
Corentin Thomasset
f66a9f5d1b feat(documents): added deleted and total metrics in the organization stats route (#556) 2025-10-14 17:59:37 +02:00
Corentin Thomasset
c5b337f3bb fix(upload): use organization-specific file size limits (#555) 2025-10-14 03:09:54 +02:00
Corentin Thomasset
bb1ba3e15e chore(release): ensure job runs only for the correct repository (#554) 2025-10-13 21:40:34 +00:00
Corentin Thomasset
ce839c4127 feat(plans): pro plan (#553) 2025-10-13 23:33:55 +02:00
Corentin Thomasset
8aabd28168 refactor(utils): removed lodash-es (#552) 2025-10-13 17:03:25 +02:00
Corentin Thomasset
1a7a14b3ed refactor(query): dropped unnecessary tanstack useQueries (#550) 2025-10-13 02:22:58 +02:00
Corentin Thomasset
17cebde051 fix(intake-emails): make email validation more permissive for webhook addresses (#548) 2025-10-12 18:56:18 +00:00
Corentin Thomasset
12ead3d017 chore(release): update versions (#535)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-12 16:50:36 +02:00
Corentin Thomasset
f6c0221858 fix(release): update Docker build trigger to use '@papra/docker' package (#546) 2025-10-12 14:35:26 +00:00
Corentin Thomasset
1aaf2c96cd fix(docker): update version from 25.10.0 to 25.9.0 and change release type to minor (#545) 2025-10-12 14:30:42 +00:00
Corentin Thomasset
9c6f14fc13 refactor(docker): dedicated package for docker management (#544)
* feat(docker): initialize Docker package with build configurations and README

* Update packages/docker/CHANGELOG.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/docker/package.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-12 16:22:38 +02:00
Corentin Thomasset
3d49962ca5 feat(docs): add architecture documentation (#543)
* feat(docs): add architecture documentation

* Update apps/docs/src/content/navigation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-11 19:39:12 +00:00
Corentin Thomasset
c434d873bc feat(organizations): soft delete organizations with recovery (#542) 2025-10-11 16:21:55 +00:00
Corentin Thomasset
60982da847 refactor(tagging-rules): enhance tagging rules repository with tag associations (#539) 2025-10-08 22:08:35 +02:00
Corentin Thomasset
73ab9e8ab5 fix(webhooks): trigger webhooks and save activity log on auto-tagging (#538) 2025-10-08 18:30:59 +00:00
Corentin Thomasset
c4a9b9b088 fix(test): forward injected date in invitation tests (#537) 2025-10-08 11:09:11 +00:00
Corentin Thomasset
9a6e822e71 feat(docker): drop support for armv7 (#532) 2025-10-06 23:56:24 +02:00
Corentin Thomasset
e52bc261db feat(organizations): added max members count check for organization invitations (#536) 2025-10-05 15:11:08 +02:00
Corentin Thomasset
624ad62c53 feat(orgs): added usage page and related components (#534)
- Implemented a new page to view organization usage, including document storage, intake emails, and member counts.
- Added translations for the new usage features in multiple languages (DE, EN, ES, FR, IT, PL, PT-BR, RO).
- Created a `UsageWarningCard` to alert users when they are nearing their storage limits.
- Updated the sidebar and organization settings layout to include a link to the usage page.
- Added API endpoints to fetch organization usage data and handle limits.
- Introduced a `ProgressCircle` component for visual representation of usage statistics.
- Refactored utility functions to handle positive infinity values in usage calculations.
2025-10-05 02:45:21 +02:00
Corentin Thomasset
630f9cc328 feat(subscriptions): add billing interval options (#533) 2025-10-04 21:57:09 +02:00
Corentin Thomasset
9f5be458fe feat(subscriptions): added cta and subscription management features (#523) 2025-10-04 14:58:42 +02:00
Corentin Thomasset
1bfdb8aa66 chore(release): update versions (#525)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-04 11:47:47 +02:00
Corentin Thomasset
2e2bb6fbbd chore(changeset): added changeset for ip env variable (#531) 2025-10-02 22:48:38 +00:00
Corentin Thomasset
d09b9ed70d feat(auth): add IP address header configuration and logging support (#530) 2025-10-03 00:41:42 +02:00
Corentin Thomasset
e1571d2b87 fix(auth): enhance logging to include additional arguments in log messages (#529) 2025-10-02 22:36:06 +00:00
Corentin Thomasset
c9a66e4aa8 fix(docs): update env variable name for OwlRelay configuration (#528) 2025-10-02 20:29:55 +00:00
Corentin Thomasset
9fa2df4235 feat(package): add module type to root package.json (#526) 2025-10-01 14:15:23 +00:00
Corentin Thomasset
c84a921988 feat(tags): update tag color validation to allow uppercase letters (#524)
* feat(tags): update tag color validation to allow uppercase letters

* Update .changeset/quiet-peas-mate.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-01 14:09:49 +00:00
Corentin Thomasset
9b5f3993c3 chore(release): update versions (#518)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-30 11:51:10 +02:00
Corentin Thomasset
b28772317c fix(file-upload): set default parameter charset to utf8 (#521) 2025-09-29 21:20:43 +02:00
Corentin Thomasset
a3f9f05c66 feat(organizations): restrict organization deletion to owners only (#517) 2025-09-26 01:49:59 +02:00
Corentin Thomasset
0616635cd6 chore(release): update versions (#509)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-24 17:00:01 +02:00
Corentin Thomasset
9e7a3ba70b chore(version): update version bump for api keys permissions changes (#516) 2025-09-24 16:49:11 +02:00
Corentin Thomasset
04990b986e docs(api-endpoints): added explications on how to use api keys (#515) 2025-09-24 14:41:14 +00:00
Corentin Thomasset
097b6bf2b7 feat(api-keys): added format check for api tokens to avoid unnecessary db call (#514) 2025-09-24 14:32:34 +00:00
Corentin Thomasset
cb3ce6b1d8 feat(api-keys): add organization permissions for api keys (#512) 2025-09-24 15:25:48 +02:00
Corentin Thomasset
405ba645f6 feat(docker): disable Better Auth telemetry in Dockerfiles (#511) 2025-09-21 20:56:43 +00:00
Corentin Thomasset
ab6fd6ad10 feat(tasks): update figue to allow for fallback task worker ids env variables (#510) 2025-09-21 22:53:04 +02:00
Corentin Thomasset
782f70ff66 feat(tasks): add option to disable PRAGMA statements in migrations (#508) 2025-09-20 22:07:34 +00:00
Corentin Thomasset
1abbf18e94 chore(release): update versions (#505)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-20 14:59:01 +02:00
Corentin Thomasset
6bcb2a71e9 feat(intake-emails): add intake email username pattern config (#506)
Co-authored-by: Alexander <goldengamerlp@users.noreply.github.com>
2025-09-19 20:37:25 +02:00
Corentin Thomasset
936bc2bd0a refactor(intake-emails): split username creation from addresses management (#504) 2025-09-18 01:59:29 +02:00
Corentin Thomasset
2efe7321cd chore(release): update versions (#494)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-14 11:31:31 +02:00
Corentin Thomasset
947bdf8385 docs(CONTRIBUTING): add IDE setup instructions for ESLint in VS Code (#502)
* docs(CONTRIBUTING): add IDE setup instructions for ESLint in VS Code

* Update CONTRIBUTING.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-14 09:18:37 +00:00
Corentin Thomasset
b5bf0cca4b fix(upload): disable client size guard when maxUploadSize <= 0 (#501) 2025-09-14 10:44:29 +02:00
Corentin Thomasset
208a561668 feat(tasks): added libsql task service driver (#500) 2025-09-13 22:42:08 +02:00
Corentin Thomasset
40cb1d71d5 fix(documents): enhance file fetching security by setting appropriate headers (#499) 2025-09-13 15:46:34 +02:00
Corentin Thomasset
3da13f7591 refactor(document-page): remove "open in new tab" button (#498) 2025-09-13 15:29:51 +02:00
Corentin Thomasset
2a444aad31 chore(tests): set timezone in vitest configurations (#497) 2025-09-13 09:25:40 +00:00
Corentin Thomasset
47d8bbd356 refactor(utils): added isString and isNonEmptyString utility functions (#495) 2025-09-12 22:22:01 +02:00
Corentin Thomasset
ed4d7e4a00 fix(folder-ingestion): allow cross docker volume file moving (#493) 2025-09-10 22:48:56 +02:00
Corentin Thomasset
f382397c0e chore(release): update versions (#489)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-10 15:38:36 +02:00
Corentin Thomasset
54514e15db fix(translations): update error messages for file size limits across multiple languages (#492) 2025-09-10 15:35:34 +02:00
Corentin Thomasset
bb9d5556d3 fix(upload): properly handle file-too-big errors (#491) 2025-09-10 14:57:46 +02:00
Corentin Thomasset
83e943c5b4 refactor(client): update favicons (#488) 2025-09-09 23:30:27 +02:00
Corentin Thomasset
40b0557553 chore(release): update versions (#465)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-09 15:06:26 +02:00
Corentin Thomasset
b5a0317d24 refactor(documents): made the document creation usecase factory synchronous (#485) 2025-09-09 09:42:33 +00:00
Corentin Thomasset
9730a06468 refactor(documents): narrowed down the document storage config parameter (#484) 2025-09-09 09:32:49 +00:00
Corentin Thomasset
ec0a437d86 fix(ingestion-folders): ensure doneFolders and errorFolders are string arrays for proper exclusion pattern (#483) 2025-09-08 23:36:04 +02:00
Corentin Thomasset
1606310745 refactor(intake-emails): update email validation to use RFC 322 compliant email for intake email origins (#481) 2025-09-04 11:53:51 +02:00
Corentin Thomasset
0a03f42231 feat(documents): implement document encryption (#480) 2025-09-04 10:15:30 +02:00
Corentin Thomasset
a62d376772 fix(tags): retreive tags affected even when only affected to deleted documents (#477)
* fix(tags): retreive tags affected even when only affected to deleted documents

* Update apps/papra-server/src/modules/tags/tags.repository.test.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .changeset/lazy-tables-cover.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-21 13:50:48 +02:00
Corentin Thomasset
ea9d90d6cf refactor(ingestion-folders): use file streaming instead of loading in ram (#475) 2025-08-20 23:01:56 +02:00
Corentin Thomasset
27a6b0b53d refactor(documents): made the creation of storage driver sync (#474) 2025-08-20 21:44:53 +02:00
Corentin Thomasset
94c4d76b86 refactor(driver): no longer use file instances in memory driver (#473) 2025-08-20 20:34:06 +02:00
Corentin Thomasset
b08241f20f refactor(server): use streaming for handling file upload (#472) 2025-08-20 20:15:57 +02:00
Corentin Thomasset
e77a42fbf1 refactor(documents): lazy load PdfViewer component for smaller main chunk (#471) 2025-08-12 22:52:00 +00:00
Corentin Thomasset
d488efe2cc refactor(i18n): simplified translation management and performance (#470) 2025-08-12 02:22:20 +02:00
Corentin Thomasset
14c3587de0 refactor(documents): improved UX of the document content update (#468) 2025-08-09 23:33:42 +02:00
Corentin Thomasset
7400a3a6ec chore(dependencies): removed unbuild (#467) 2025-08-09 20:05:48 +00:00
Corentin Thomasset
14bc2b8f8d chore(dependencies): replace unbuild with tsdown (#464) 2025-08-09 17:03:14 +02:00
Corentin Thomasset
e48745331f chore(ci): mutualized CI workflows (#463) 2025-08-09 16:43:43 +02:00
Corentin Thomasset
38dcc765f9 chore(release): update versions (#462)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-09 11:39:03 +02:00
Corentin Thomasset
c085b9d676 fix(documents): apply tagging rules after the content is extracted (#461) 2025-08-09 11:36:33 +02:00
Corentin Thomasset
a64a93544d chore(release): update versions (#460)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-09 02:11:20 +02:00
Corentin Thomasset
f20559e95d chore(migrations): update migration command for production environment (#459) 2025-08-09 00:07:40 +00:00
Corentin Thomasset
7d755d0981 chore(docker): add build dependencies for sharp/node-gyp in Dockerfile (#457) 2025-08-09 02:01:44 +02:00
Corentin Thomasset
5382019721 chore(release): update versions (#420)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-08 21:16:08 +02:00
Corentin Thomasset
b33fde35d3 feat(auth): improved feedback for invalid origin url (#455) 2025-08-08 18:10:54 +02:00
Corentin Thomasset
fd6f83f538 refactor(migrations): purged legacy migrations (#453) 2025-08-08 02:31:13 +02:00
Corentin Thomasset
7f7e5bffcb refactor(database): completely rewrote the db migration tooling (#452) 2025-08-08 02:18:22 +02:00
Corentin Thomasset
5868800bce fix(tags): fixed the impossibility to delete a tag that have been affected to a document (#448)
* fix(tags): fixed the impossibility to delete a tag that have been affected to a document

- Added user feedback for errors encountered during tag deletion in the client.
- Updated localization files to include new error messages for internal processing issues across multiple languages.
- Modified the document activity log migration to set foreign keys to null on delete for user and tag references.

* Update .changeset/green-teeth-fall.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .changeset/cuddly-shoes-watch.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-02 00:41:43 +02:00
Corentin Thomasset
b5ccc135ba refactor(documents): document content extraction is now async (#447)
* refactor(documents): implement asynchronous document content extraction

- Updated dependencies for `@cadence-mq/core` and `@cadence-mq/driver-memory` to versions 0.2.1 and 0.2.0 respectively.
- Introduced a new task for extracting document file content asynchronously.
- Refactored document creation use case to schedule the extraction task.
- Added utility functions for stream conversion and text extraction from files.
- Updated relevant tests to accommodate the new asynchronous behavior and task services integration.

* Update .changeset/cyan-pots-begin.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-01 21:44:29 +00:00
Manuel Zavatta
5e46bb9e6a feat(i18n): added Italian translation
* Create it.yml

cloned from en.yml

* Update it.yml

italian translation

* Update i18n.constants.ts

* fix(i18n): lint and auto order

* chore(versioning): added changeset

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-07-31 21:15:43 +00:00
Corentin Thomasset
41a113334a refactor(tasks): integrated cadence task services (#436) 2025-07-28 18:30:11 +00:00
Corentin Thomasset
6723baf98a feat(webhooks): add document update and tag events (#432) 2025-07-25 16:46:05 +02:00
Corentin Thomasset
bbe5fe74e2 test(lecture): added fixture test timeout (#431) 2025-07-25 12:56:46 +00:00
Corentin Thomasset
a8cff8cedc refactor(webhooks): updated webhooks signatures and payload to match standard-webhook spec (#430) 2025-07-25 11:29:26 +02:00
Corentin Thomasset
67b3b14cdf feat(lecture): added ocr support for scanned pdf (#429) 2025-07-24 22:21:10 +02:00
Osaf Ali Sayed
ffdae8db56 feat(intake-emails): redesigned intake email list (#412)
* feat(intake-emails): redesigned intake email list

* fix(intake-emails): fix linting

* fix(intake-emails): set drop down menu trigger size same as icon

* chore(version): added changeset

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-07-14 13:28:48 +00:00
Edward205
7768840aa4 refactor(i18n): improved Romanian translation (#419)
* added diacritics and improved wording

* chore(version): added changeset

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-07-14 11:36:10 +00:00
Corentin Thomasset
dd3862e50c chore(release): update versions (#418)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-13 22:45:48 +02:00
Corentin Thomasset
a82ff3a755 chore(docker): add lecture package.json to Dockerfiles (#417) 2025-07-13 20:42:55 +00:00
Corentin Thomasset
d5b00307da chore(dependencies): put unbuild in pnpm catalog (#416) 2025-07-13 20:19:12 +00:00
Corentin Thomasset
5ce21981a9 chore(release): update versions (#370)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-13 21:53:55 +02:00
Corentin Thomasset
3401cfbfdc feat(config): introduce appBaseUrl for overriding client and server base URLs (#405)
- Added appBaseUrl configuration to allow users to set a base URL that overrides both client and server base URLs.
- Updated documentation to reflect the new configuration variable and its usage.
- Refactored relevant code to utilize appBaseUrl where applicable, ensuring consistent behavior across the application.
- Enhanced tests to verify the correct application of appBaseUrl in various scenarios.
2025-07-13 21:46:07 +02:00
Adrian Ortiz
26015666de feat(i18n): add Spanish language support (#411)
* feat(i18n): add Spanish language support to the app

- Add es.yml localization file with full Spanish translations
- Register 'es' language in the i18n locales constant

* Create shiny-dancers-count.md

* style(es.yml): fix formatting and spacing in es.yml

- Remove unnecessary quotes in 'organizations.create-first.user-name'
- Fix extra space in the '# API keys' comment

* Update .changeset/shiny-dancers-count.md

---------

Co-authored-by: Adrian Ortiz <desarrollador3@en-trega.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-07-09 21:41:04 +00:00
Corentin Thomasset
09e3bc5e15 feat(i18n): finalize Romanian translationsetup (#408)
* feat: Romanian translation

* chore(i18n): finalized Romanian translation

---------

Co-authored-by: Razvan M. <76774976+iRazvan2745@users.noreply.github.com>
2025-07-09 00:32:04 +02:00
Piotr Icikowski
1711ef866d chore(i18n): added Polish translations (#403)
* feat(i18n): add Polish translation

* chore(version): added changeset

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-07-08 00:01:18 +02:00
Corentin Thomasset
1d23f40894 fix(docs): update schema URL in configuration examples to use the correct domain (#402) 2025-07-07 12:00:35 +00:00
juoum
40a1f91b67 feat(i18n): Added European Portuguese (pt) translation (#391)
* Add European Portuguese (pt) translation

* chore: auto lint

* chore: added changeset

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-07-04 23:12:27 +02:00
Corentin Thomasset
47b69b15f4 fix(organization-settings): update back button link to use organization ID in URL (#399) 2025-07-04 23:03:19 +02:00
Corentin Thomasset
a188af1f88 chore(lint): enabled type-aware linting (#398) 2025-07-04 22:55:42 +02:00
Corentin Thomasset
f28d8245bf feat(auth): added configuration to disable auth by email (#394) 2025-07-02 13:36:19 +02:00
Corentin Thomasset
aad36f3252 fix(documents): corrected weird centering for long file names (#393) 2025-07-01 22:01:53 +00:00
Corentin Thomasset
21a5ccce6d fix(docker): set COREPACK_HOME for rootless image to avoid permission issues (#392) 2025-07-01 23:25:24 +02:00
Corentin Thomasset
42bc3c6698 feat(docs): added api endpoint doc page (#390) 2025-07-01 18:47:41 +02:00
Corentin Thomasset
a9f474dc2d Merge pull request #388 from papra-hq/lecture-integration
chore(setup): integrated lecture package in the monorepo
2025-06-30 21:20:13 +02:00
Corentin Thomasset
ed5a93cb47 chore: update repository URLs and clean up package configurations 2025-06-30 21:17:22 +02:00
Corentin Thomasset
52df988c02 Add 'packages/lecture/' from commit '9b2a4b2ae90de0cc5ba1c7a3f14b308185e9c705'
git-subtree-dir: packages/lecture
git-subtree-mainline: 73b8d08076
git-subtree-split: 9b2a4b2ae9
2025-06-30 21:03:28 +02:00
Corentin Thomasset
73b8d08076 feat(documents): added configuration for the ocr languages (#387) 2025-06-29 22:14:58 +02:00
Corentin Thomasset
9b2a4b2ae9 chore: release v0.0.7 2025-06-29 16:08:54 +02:00
Corentin Thomasset
2a8b88e48a refactor(extractors): added config in high-order extraction methods (#4) 2025-06-29 14:08:11 +00:00
Corentin Thomasset
be1b70a26a chore: release v0.0.6 2025-06-29 15:56:28 +02:00
Corentin Thomasset
1755849483 refactor(config): rename and export ocrLanguages (#3) 2025-06-29 15:54:22 +02:00
Corentin Thomasset
b3693fd9c9 chore: release v0.0.5 2025-06-29 15:01:14 +02:00
Corentin Thomasset
2149b50270 feat(config): added the possibility to configure tesseract ocr (#2) 2025-06-29 15:00:37 +02:00
Lucas Arantes
0b276ee0d5 feat(i18n): add Brazilian Portuguese (pt-BR) translation (#383)
* feat(i18n): add Portuguese (pt-BR) translation

* fix(i18n): remove trailing space in pt-BR.yml

* chore: added changeset

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-06-26 22:31:20 +00:00
Corentin Thomasset
56fb9ec2c4 docs(CONTRIBUTING): update dev instructions with package build step (#382) 2025-06-25 13:37:12 +02:00
Corentin Thomasset
6cedc30716 chore(deps): updated dependencies (#379) 2025-06-24 20:52:15 +02:00
Corentin Thomasset
f1e1b4037b feat(tags): add color picker and swatches for tag creation (#378) 2025-06-24 20:27:58 +02:00
Corentin Thomasset
205c6cfd46 feat(preview): improved document preview for text-like files (#377) 2025-06-24 00:11:40 +02:00
Alex
c54a71d2c5 fix(tags): allow for uppercase tag color code (#346)
* Update tags.page.tsx

* Fixes 400 error when submitting tags with uppercase hex colour codes.

Fixes 400 error when submitting tags with uppercase hex colour codes.

* Update .changeset/few-toes-ask.md

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-06-19 11:45:06 +02:00
Corentin Thomasset
62b7f0382c chore(release): update versions (#358)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-18 22:11:19 +02:00
Corentin Thomasset
57c6a26657 fix(demo): case insensitive dummy search in demo (#367) 2025-06-18 19:03:10 +00:00
Corentin Thomasset
b8c2bd70e3 feat(tags): allow for adding/removing tags to document using api keys (#366) 2025-06-18 20:58:03 +02:00
Marvin Deuschle
0c2cf698d1 feat(i18n): added German translation (#359)
* feat: Add german translation

* fix: Added changeset entry

* Update apps/papra-client/src/locales/de.yml

* Update apps/papra-client/src/locales/de.yml

* Update apps/papra-client/src/locales/de.yml

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2025-06-15 21:51:13 +02:00
Corentin Thomasset
585c53cd9d chore(changesets): added /llms.txt announcement changesets (#357) 2025-06-14 19:16:28 +02:00
Corentin Thomasset
f035458e16 feat(docs): added descriptions in docs-navigation.json (#354) 2025-06-14 00:37:47 +02:00
Corentin Thomasset
556fd8b167 feat(docs): added navigation json export (#341) 2025-06-10 21:30:56 +02:00
Corentin Thomasset
81e85295ba chore(release): update versions (#334)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-07 17:40:28 +02:00
Corentin Thomasset
1c574b8305 feat(script): ensure local database directory exists before running scripts (#337) 2025-06-07 17:26:28 +02:00
Corentin Thomasset
ff830c234a fix(client): corrected version release link (#333) 2025-06-07 15:09:08 +02:00
Corentin Thomasset
451564f354 docs(readme): updated features statuses (#328) 2025-06-07 14:58:21 +02:00
Corentin Thomasset
ecd6af45c8 docs(README): update project status and add sponsorship section (#327) 2025-06-06 22:04:49 +00:00
Corentin Thomasset
cb652c7166 chore(release): update versions (#323)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-04 21:32:34 +02:00
Corentin Thomasset
17ca8f8f81 fix(documents): update Content-Disposition header to support UTF-8 encoded filenames (#326) 2025-06-04 21:30:06 +02:00
Corentin Thomasset
f54b8e162a feat(docs): auto compute urls from port in dc generator (#322) 2025-06-04 13:47:52 +02:00
Corentin Thomasset
6b435bba79 chore(release): update versions (#305)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-04 00:09:45 +02:00
Corentin Thomasset
8ccdb74834 refactor(docker): added base url override in docker (#320) 2025-06-03 22:04:15 +00:00
Corentin Thomasset
60059c895c feat(invitations): add invitations management page (#319) 2025-06-03 22:13:21 +02:00
Corentin Thomasset
6e22a93dff feat(locales): add fr translations for document activity logging (#318) 2025-05-30 13:58:00 +02:00
Corentin Thomasset
79c1d3206b feat(documents): added document activity logging (#317) 2025-05-30 13:45:25 +02:00
Corentin Thomasset
48a953a584 refactor(client): migrated tanstack createQuery and createMutation to useQuery and useMutation (#316) 2025-05-28 21:51:30 +02:00
Corentin Thomasset
fdb90fa164 feat(tags): add error handling for existing tags (#315) 2025-05-27 21:09:46 +02:00
Corentin Thomasset
e9a205c0a3 feat(documents): added document renaming (#314) 2025-05-27 20:11:05 +02:00
Corentin Thomasset
278db63fc8 chore(deps): updated some dependencies version (#313) 2025-05-27 13:46:43 +02:00
Corentin Thomasset
e5ef40f36c chore(version): added missing changeset for password reset fix (#312) 2025-05-26 20:30:24 +00:00
Corentin Thomasset
27c9e39422 fix(auth): fix deprecated better-auth database id generation conf (#311) 2025-05-26 20:27:32 +00:00
Corentin Thomasset
91d2e236d0 fix(auth): corrected password reset navigation guard (#310) 2025-05-26 22:19:33 +02:00
Corentin Thomasset
d4f72e889a refactor(client): hide manage subscription section (#309) 2025-05-26 21:42:19 +02:00
Corentin Thomasset
759a3ff713 feat(i18n): extracted hard coded text for i18n (#308) 2025-05-26 01:14:43 +02:00
Corentin Thomasset
34862991fb chore(cf): added security headers in docs and papra-client (#307) 2025-05-25 12:05:38 +00:00
Corentin Thomasset
f0876fdc63 feat(server): added smtp client support for emailing (#306) 2025-05-25 11:47:12 +02:00
Corentin Thomasset
cb38d66485 refactor(emails): restructure emails service to support multiple drivers (#304) 2025-05-25 01:26:28 +02:00
Corentin Thomasset
c28af1407f chore(release): update versions (#303)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-24 19:58:09 +02:00
Corentin Thomasset
b62ddf2bc4 chore(docker): add EMAILS_DRY_RUN environment variable to Dockerfiles (#302) 2025-05-24 17:55:59 +00:00
Corentin Thomasset
fa7909c62d chore(release): add 'actions' permission to changeset workflow (#301) 2025-05-24 17:38:19 +00:00
Corentin Thomasset
1996b51b4d chore(release): update versions (#292)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-24 18:17:41 +02:00
Corentin Thomasset
734027f00c feat(docs): updated feature list statuses (#300) 2025-05-24 18:08:32 +02:00
Corentin Thomasset
557cde940c feat(organizations): added member role update functionality (#299) 2025-05-24 17:13:32 +02:00
Corentin Thomasset
26a83052bd fix(intake-emails): enhance disabled intake-email state (#298) 2025-05-24 12:27:09 +00:00
Corentin Thomasset
5aac3f7ba6 fix(demo): added missing routes in demo (#297) 2025-05-24 12:16:05 +00:00
Corentin Thomasset
0ddc2340f0 fix(locales): update registration page description (#296) 2025-05-24 11:24:44 +00:00
Corentin Thomasset
438a31171c feat(auth): added support for custom oauth2 providers (#295) 2025-05-24 03:12:39 +02:00
Corentin Thomasset
53bf93f128 feat(doc): added a papra docker compose generator (#293) 2025-05-23 21:24:08 +00:00
Corentin Thomasset
b400b3f18d feat(database): ensure local database directory en boot (#294) 2025-05-23 22:21:33 +02:00
Corentin Thomasset
0627ec25a4 feat(organizations): add permission check for invitation (#291) 2025-05-21 23:06:43 +02:00
Corentin Thomasset
72e5a9a4de feat(invitations): added organizations invitations and multi-user (#289) 2025-05-21 21:53:56 +02:00
Corentin Thomasset
268ac8e358 chore(release): update Docker release workflow to use version input parameter (#286) 2025-05-14 13:11:19 +02:00
Corentin Thomasset
f37c7dd8f7 chore: release v0.0.4 2025-01-22 15:08:50 +01:00
Corentin Thomasset
a7fbf21a9f docs(readme): added images in supported file formats 2025-01-22 14:03:22 +01:00
Corentin Thomasset
f97e5f863e feat(extractors): add image extractor 2025-01-22 13:59:19 +01:00
Corentin Thomasset
8dcd6bc5ed feat(extractors): improved tests 2025-01-22 13:18:54 +01:00
Corentin Thomasset
87cb325369 fix(extractor): corrected name of text extractor 2025-01-22 13:11:39 +01:00
Corentin Thomasset
e1743954d2 chore: release v0.0.3 2025-01-22 11:40:18 +01:00
Corentin Thomasset
44b5b9fd5a refactor(interface): normalized api 2025-01-22 11:39:54 +01:00
Corentin Thomasset
68c5a3e2b7 refactor(npm): auto format package.json 2025-01-22 11:39:12 +01:00
Corentin Thomasset
684138c3fd chore(npm): added keywords in package.json 2025-01-22 04:30:55 +01:00
Corentin Thomasset
0aa3241712 chore: release v0.0.2 2025-01-22 04:29:02 +01:00
Corentin Thomasset
ad6358195e chore(cd): added actions for npm release 2025-01-22 04:27:44 +01:00
Corentin Thomasset
0e99669206 docs(readme): update README to include usage in Papra project 2025-01-22 04:18:34 +01:00
Corentin Thomasset
a91d98fb44 chore(setup): first commit 2025-01-22 04:16:54 +01:00
618 changed files with 48891 additions and 8100 deletions

View File

@@ -5,13 +5,11 @@
{ "repo": "papra-hq/papra"}
],
"commit": false,
"fixed": [
["@papra/app-client", "@papra/app-server"]
],
"fixed": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"ignore": ["@papra/app-client", "@papra/app-server", "@papra/docs"],
"privatePackages": {
"tag": true,
"version": true

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Added deleted and total document counts and sizes in the `/api/organizations/:organizationId/documents/statistics` route

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Lighten the client bundle by removing lodash dep

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Fix weird navigation freeze when direct navigation to organizations

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Made the validation more permissive for incoming intake email webhook addresses, allowing RFC 5322 compliant email addresses instead of just simple emails.

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Prevent small flash of wrong theme on initial load for slower connections

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Redacted webhook signing secret in api update response

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Reduced the client bundle size by switching to posthog-lite

View File

@@ -0,0 +1,5 @@
---
"@papra/docker": patch
---
Use organization max file size limit for pre-upload validation

View File

@@ -1,11 +0,0 @@
node_modules
.pnp
.pnp.*
*.log
dist
*.local
.git
db.sqlite
local-documents
.env
**/.env

1
.dockerignore Symbolic link
View File

@@ -0,0 +1 @@
packages/docker/.dockerignore

View File

@@ -1,42 +0,0 @@
name: CI - Docs
on:
pull_request:
push:
branches:
- main
jobs:
ci-apps-docs:
name: CI - Docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/docs
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
working-directory: ./
- name: Run linters
run: pnpm lint
# - name: Type check
# run: pnpm typecheck
# - name: Run unit test
# run: pnpm test
- name: Build the app
run: pnpm build

View File

@@ -1,49 +0,0 @@
name: CI - App Client
on:
pull_request:
push:
branches:
- main
jobs:
ci-apps-papra-client:
name: CI - Papra Client
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/papra-client
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
working-directory: ./
- name: Run linters
run: pnpm lint
- name: Type check
run: pnpm typecheck
- name: Run unit test
run: pnpm test
# Ensure locales types are up to date, must be run before building the app
- name: Check locales types
run: |
pnpm script:generate-i18n-types
git diff --exit-code -- src/modules/i18n/locales.types.ts > /dev/null || (echo "Locales types are outdated, please run 'pnpm script:generate-i18n-types' and commit the changes." && exit 1)
- name: Build the app
run: pnpm build

View File

@@ -1,43 +0,0 @@
name: CI - App Server
on:
pull_request:
push:
branches:
- main
jobs:
ci-apps-papra-server:
name: CI - Papra Server
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/papra-server
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: |
pnpm i --frozen-lockfile
pnpm --filter "@papra/app-server^..." build
- name: Run linters
run: pnpm lint
- name: Type check
run: pnpm typecheck
- name: Run unit test
run: pnpm test
- name: Build the app
run: pnpm build

View File

@@ -1,41 +0,0 @@
name: CI - Api SDK
on:
pull_request:
push:
branches:
- main
jobs:
ci-packages-api-sdk:
name: CI - Api SDK
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/api-sdk
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Run linters
run: pnpm lint
- name: Type check
run: pnpm typecheck
# - name: Run unit test
# run: pnpm test
- name: Build the app
run: pnpm build

View File

@@ -1,44 +0,0 @@
name: CI - CLI
on:
pull_request:
push:
branches:
- main
jobs:
ci-packages-cli:
name: CI - CLI
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/cli
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Build related packages
run: cd ../api-sdk && pnpm build
- name: Run linters
run: pnpm lint
- name: Type check
run: pnpm typecheck
# - name: Run unit test
# run: pnpm test
- name: Build the app
run: pnpm build

View File

@@ -1,41 +0,0 @@
name: CI - Webhooks
on:
pull_request:
push:
branches:
- main
jobs:
ci-packages-webhooks:
name: CI - Webhooks
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/webhooks
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Run linters
run: pnpm lint
- name: Type check
run: pnpm typecheck
- name: Run unit test
run: pnpm test
- name: Build the app
run: pnpm build

47
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
ci:
name: CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
# Build only the packages first to properly run the lint and test first to get faster feedback
- name: Build packages
run: pnpm -r --parallel -F "./packages/*" build
- name: Run linters
run: pnpm -r --parallel lint
- name: Type check
# Exclude docs as their are some typing issues we are ok with for now
run: pnpm -r --parallel -F "!@papra/docs" typecheck
# Tests are run using vitest projects
- name: Run tests
run: pnpm test
# Now build the apps, the longer step, so we do it last as they are more unlikely to fail if the previous steps works
- name: Build the apps
run: pnpm -r --parallel -F "./apps/*" build
- name: Ensure no non-excluded files are changed for the whole repo
run: git diff --exit-code > /dev/null || (echo "After running the CI, some un-committed changes were detected. Please ensure cleanness before merging." && exit 1)

View File

@@ -1,9 +1,12 @@
name: Release new versions
name: Build and publish Docker images
on:
push:
tags:
- '@papra/app-server@*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g. 0.8.2)'
required: true
type: string
permissions:
contents: read
@@ -11,12 +14,9 @@ permissions:
jobs:
docker-release:
name: Release Docker images
name: Build and publish Docker images
runs-on: ubuntu-latest
steps:
- name: Get release version from tag
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/@papra/app-server@}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -43,26 +43,26 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./packages/docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
corentinth/papra:latest-root
corentinth/papra:${{ env.RELEASE_VERSION }}-root
corentinth/papra:${{ inputs.version }}-root
ghcr.io/papra-hq/papra:latest-root
ghcr.io/papra-hq/papra:${{ env.RELEASE_VERSION }}-root
ghcr.io/papra-hq/papra:${{ inputs.version }}-root
- name: Build and push rootless Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile.rootless
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./packages/docker/Dockerfile.rootless
platforms: linux/amd64,linux/arm64
push: true
tags: |
corentinth/papra:latest
corentinth/papra:latest-rootless
corentinth/papra:${{ env.RELEASE_VERSION }}-rootless
corentinth/papra:${{ inputs.version }}-rootless
ghcr.io/papra-hq/papra:latest
ghcr.io/papra-hq/papra:latest-rootless
ghcr.io/papra-hq/papra:${{ env.RELEASE_VERSION }}-rootless
ghcr.io/papra-hq/papra:${{ inputs.version }}-rootless

View File

@@ -11,10 +11,12 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
if: github.repository == 'papra-hq/papra'
permissions:
contents: write
pull-requests: write
id-token: write
actions: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -40,4 +42,13 @@ jobs:
title: "chore(release): update versions"
env:
GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Trigger Docker build
if: steps.changesets.outputs.published == 'true' && contains(fromJson(steps.changesets.outputs.publishedPackages).*.name, '@papra/docker')
run: |
VERSION=$(echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[] | select(.name=="@papra/docker") | .version')
echo "VERSION: $VERSION"
gh workflow run release-docker.yaml -f version="$VERSION"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

7
.gitignore vendored
View File

@@ -35,8 +35,13 @@ cache
*.db-shm
*.db-wal
*.sqlite
*.sqlite-shm
*.sqlite-wal
local-documents
ingestion
.cursorrules
*.traineddata
*.traineddata
.eslintcache
.claude

212
CLAUDE.md Normal file
View File

@@ -0,0 +1,212 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Papra is a minimalistic document management and archiving platform built as a monorepo using PNPM workspaces. The project includes a SolidJS frontend, HonoJS backend, CLI tools, and supporting packages.
It's open-source and designed for easy self-hosting using Docker, and also offers a cloud-hosted SaaS version.
## Architecture
### Monorepo Structure
- **apps/papra-server**: Backend API server (HonoJS + Drizzle ORM + Better Auth)
- **apps/papra-client**: Frontend application (SolidJS + UnoCSS + Shadcn Solid)
- **apps/docs**: Documentation site (Astro + Starlight)
- **packages/lecture**: Text extraction library for documents
- **packages/api-sdk**: API client SDK
- **packages/cli**: Command-line interface
- **packages/webhooks**: Webhook types and utilities
### Backend Architecture (apps/papra-server)
The backend follows a modular architecture with feature-based modules:
- **Module pattern**: Each feature lives in `src/modules/<feature>/` with:
- `*.repository.ts`: Database access layer (Drizzle ORM queries)
- `*.usecases.ts`: Business logic and orchestration
- `*.routes.ts`: HTTP route handlers (Hono)
- `*.services.ts`: Service layer for external integrations
- `*.table.ts`: Drizzle schema definitions
- `*.types.ts`: TypeScript type definitions
- `*.errors.ts`: Error definitions
- **Core modules**: `app`, `shared`, `config`, `tasks`
- **Feature modules**: `documents`, `organizations`, `users`, `tags`, `tagging-rules`, `intake-emails`, `ingestion-folders`, `webhooks`, `api-keys`, `subscriptions`, etc.
- **Database**: Uses Drizzle ORM with SQLite/Turso (libsql). Schema is in `*.table.ts` files, migrations in `src/migrations/`
- **Authentication**: Better Auth library for user auth
- **Task system**: Background job processing using Cadence MQ, a custom made queue system (papra-hq/cadence-mq)
- **Document storage**: Abstracted storage supporting local filesystem, S3, and Azure Blob
### Frontend Architecture (apps/papra-client)
- **SolidJS** for reactivity with router (`@solidjs/router`)
- **Module pattern**: Features in `src/modules/<feature>/` with:
- `components/`: UI components
- `pages/`: Route components
- `*.services.ts`: API client calls
- `*.provider.tsx`: Context providers
- `*.types.ts`: Type definitions
- **Routing**: Defined in `src/routes.tsx`
- **Styling**: UnoCSS for atomic CSS with Shadcn Solid components
- **State**: TanStack Query for server state, local storage for client state
- **i18n**: TypeScript-based translations in `src/locales/*.dictionary.ts`
### Dependency Injection Pattern
The server uses a dependency injection pattern with `@corentinth/chisels/injectArguments` to create testable services that accept dependencies as parameters.
## Development Commands
### Initial Setup
```bash
# Install dependencies
pnpm install
# Build all packages (required before running apps)
pnpm build:packages
```
### Backend Development
```bash
cd apps/papra-server
# Run database migrations
pnpm migrate:up
# Start development server (localhost:1221)
pnpm dev
# Run tests
pnpm test # All tests
pnpm test:watch # Watch mode
pnpm test:unit # Unit tests only
pnpm test:int # Integration tests only
# Lint and typecheck
pnpm lint
pnpm typecheck
# Database management
pnpm db:studio # Open Drizzle Studio
pnpm migrate:create "migration_name" # Create new migration
```
### Frontend Development
```bash
cd apps/papra-client
# Start development server (localhost:3000)
pnpm dev
# Run tests
pnpm test
pnpm test:watch
pnpm test:e2e # Playwright E2E tests
# Lint and typecheck
pnpm lint
pnpm typecheck
# i18n key synchronization
pnpm script:sync-i18n-key-order
```
### Package Development
```bash
cd packages/<package-name>
# Build package
pnpm build
pnpm build:watch # Watch mode (or pnpm dev)
# Run tests
pnpm test
pnpm test:watch
```
### Root-level Commands
```bash
# Run tests across all packages
pnpm test
pnpm test:watch
# Build all packages
pnpm build:packages
# Version management (changesets)
pnpm changeset # Create changeset
pnpm version # Apply changesets and bump versions
# Docker builds
pnpm docker:build:root
pnpm docker:build:root:amd64
pnpm docker:build:root:arm64
```
### Documentation Development
```bash
cd apps/docs
pnpm dev # localhost:4321
```
## Testing Guidelines
- Use **Vitest** for all testing
- Test files: `*.test.ts` for unit tests, `*.int.test.ts` for integration tests
- Use business-oriented test names (avoid `it('should return true')`)
- Integration tests may use Testcontainers (Azurite, LocalStack)
- All new features require test coverage
## Code Style
- **ESLint config**: `@antfu/eslint-config` (auto-fix on save recommended)
- **Conventions**:
- Use functional programming where possible
- Prefer clarity and maintainability over performance
- Use meaningful names for variables, functions, and components
- Follow Conventional Commits for commit messages
- **Type safety**: Strict TypeScript throughout
## i18n
- Language files in `apps/papra-client/src/locales/*.dictionary.ts`
- Reference `en.dictionary.ts` for all keys (English is fallback)
- Fully type-safe with TypeScript
- Update `i18n.constants.ts` when adding new languages
- Use `pnpm script:sync-i18n-key-order` to sync key order
- **Branchlet/core**: Uses `@branchlet/core` for pluralization and conditional i18n string templates (variant of ICU message format)
- Basic interpolation: `'Hello {{ name }}!'` with `{ name: 'World' }`
- Conditionals: `'{{ count, =0:no items, =1:one item, many items }}'`
- Pluralization with variables: `'{{ count, =0:no items, =1:{count} item, {count} items }}'`
- Range conditions: `'{{ score, [0-50]:bad, [51-75]:good, [76-100]:excellent }}'`
- See [branchlet documentation](https://github.com/CorentinTh/branchlet) for more details
## Contributing Flow
1. Open an issue before submitting PRs for features/bugs
2. Target the `main` branch (continuously deployed to production)
3. Keep PRs small and atomic
4. Ensure CI is green (linting, type checking, testing, building)
5. PRs are squashed on merge
## Key Technologies
- **Frontend**: SolidJS, UnoCSS, Shadcn Solid, TanStack Query, Vite
- **Backend**: HonoJS, Drizzle ORM, Better Auth, Zod, Cadence MQ
- **Database**: SQLite/Turso (libsql)
- **Testing**: Vitest, Playwright, Testcontainers
- **Monorepo**: PNPM workspaces with catalog for shared dependencies
- **Build**: esbuild (backend), Vite (frontend), tsdown (packages)

View File

@@ -43,9 +43,9 @@ We welcome contributions to improve and expand the app's internationalization (i
### Adding a New Language
1. **Create a Language File**: To add a new language, create a YAML file named with the appropriate [ISO language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g., `fr.yml` for French) in the [`apps/papra-client/src/locales`](./apps/papra-client/src/locales) directory.
1. **Create a Language File**: To add a new language, create a TypeScript file named with the appropriate [ISO language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) followed by `.dictionary.ts` (e.g., `fr.dictionary.ts` for French) in the [`apps/papra-client/src/locales`](./apps/papra-client/src/locales) directory.
2. **Use the Reference File**: Refer to the [`en.yml`](./apps/papra-client/src/locales/en.yml) file, which contains all keys used in the app. Use it as a base to ensure consistency when creating your new language file. And act as a fallback if a key is missing in the new language file.
2. **Use the Reference File**: Refer to the [`en.dictionary.ts`](./apps/papra-client/src/locales/en.dictionary.ts) file, which contains all keys used in the app. Use it as a base to ensure consistency when creating your new language file. The English translations act as a fallback if a key is missing in the new language file.
3. **Update the Locale List**: After adding the new language file, include the language code in the `locales` array found in the [`apps/papra-client/src/modules/i18n/i18n.constants.ts`](./apps/papra-client/src/modules/i18n/i18n.constants.ts) file.
@@ -53,14 +53,21 @@ We welcome contributions to improve and expand the app's internationalization (i
### Updating an Existing Language
If you want to update an existing language file, you can do so directly in the corresponding JSON file in the [`apps/papra-client/src/locales`](./apps/papra-client/src/locales) directory. If you're adding or removing keys in the default language file ([`en.yml`](./apps/papra-client/src/locales/en.yml)), please run the following command to update the types (used for type checking the translations keys in the app):
If you want to update an existing language file, you can do so directly in the corresponding TypeScript file in the [`apps/papra-client/src/locales`](./apps/papra-client/src/locales) directory. The translation keys are now fully type-safe with TypeScript, so you'll get immediate feedback if you add invalid keys or have syntax errors.
```bash
pnpm script:generate-i18n-types
```
> [!TIP]
> You can use the command `pnpm script:sync-i18n-key-order` to sync the order of the keys in the TypeScript i18n files, it'll also add the missing keys as comments.
- This command will update the file [`locales.types.ts`](./apps/papra-client/src/modules/i18n/locale.types.ts) with the new/removed keys.
- When developing in papra-client (using `pnpm dev`), the i18n types definition will automatically update when you touch the [`en.yml`](./apps/papra-client/src/locales/en.yml) file, so no need to run the command above.
### Using Branchlet for Pluralization and Conditionals
Papra uses [`@branchlet/core`](https://github.com/CorentinTh/branchlet) for pluralization and conditional i18n string templates (a variant of ICU message format). Here are some common patterns:
- **Basic interpolation**: `'Hello {{ name }}!'` with `{ name: 'World' }`
- **Conditionals**: `'{{ count, =0:no items, =1:one item, many items }}'`
- **Pluralization with variables**: `'{{ count, =0:no items, =1:{count} item, {count} items }}'`
- **Range conditions**: `'{{ score, [0-50]:bad, [51-75]:good, [76-100]:excellent }}'`
See the [branchlet documentation](https://github.com/CorentinTh/branchlet) for more details on syntax and advanced usage.
## Development Setup
@@ -81,7 +88,15 @@ We recommend running the app locally for development. Follow these steps:
pnpm install
```
3. Start the development server for the backend:
3. Build the monorepo packages:
As the apps rely on internal packages, you need to build them first.
```bash
pnpm build:packages
```
4. Start the development server for the backend:
```bash
cd apps/papra-server
@@ -91,7 +106,7 @@ We recommend running the app locally for development. Follow these steps:
pnpm dev
```
4. Start the frontend:
5. Start the frontend:
```bash
cd apps/papra-client
@@ -99,7 +114,74 @@ We recommend running the app locally for development. Follow these steps:
pnpm dev
```
5. Open your browser and navigate to `http://localhost:3000`.
6. Open your browser and navigate to `http://localhost:3000`.
### IDE Setup
#### ESLint Extension
We recommend installing the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for VS Code to get real-time linting feedback and automatic code fixing.
The linting configuration is based on [@antfu/eslint-config](https://github.com/antfu/eslint-config), you can find specific IDE configurations in their repository.
<details>
<summary>Recommended VS Code Settings</summary>
Create or update your `.vscode/settings.json` file with the following configuration:
```json
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in your IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}
```
</details>
### Testing

View File

@@ -39,12 +39,9 @@ A live demo of the platform is available at [demo.papra.app](https://demo.papra.
## Project Status
Papra is currently in **beta**. The core functionality is stable and usable, but you may encounter occasional bugs or limitations. The project is actively developed, with new features being added regularly.
Papra is under active development, the core functionalities are stable and usable. With lots of features and improvements added regularly.
- ✅ Core document management features are stable
- ✅ Self-hosting is fully supported
- 🚧 Some advanced features are still in development
- 📝 Feedback and bug reports are highly appreciated
Feedback and bug reports are highly appreciated to help us improve the platform.
## Features
@@ -61,13 +58,19 @@ Papra is currently in **beta**. The core functionality is stable and usable, but
- **Content extraction**: Automatically extract text from images or scanned documents for search.
- **Tagging Rules**: Automatically tag documents based on custom rules.
- **Folder ingestion**: Automatically import documents from a folder.
- *In progress:* **i18n**: Support for multiple languages.
- *Coming soon:* **SDK and API**: Build your own applications on top of Papra.
- *Coming soon:* **CLI**: Manage your documents from the command line.
- **CLI**: Manage your documents from the command line.
- **API, SDK and webhooks**: Build your own applications on top of Papra.
- **i18n**: Support for multiple languages.
- *Coming soon:* **Document sharing**: Share documents with others.
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.
- *Coming maybe one day:* **Mobile app**: Access and upload documents on the go.
- *Coming maybe one day:* **Desktop app**: Access and upload documents from your computer.
- *Coming maybe one day:* **Browser extension**: Upload documents from your browser.
- *Coming maybe one day:* **AI**: Use AI to help you manage or tag your documents.
## Sponsors
Papra is a free and open-source project, but it is not free to run and develop. If you want to support the project, you can become a sponsor on [GitHub Sponsors](https://github.com/sponsors/corentinth) or [Buy me a coffee](https://buymeacoffee.com/cthmsst). If you are a company, you can also contact me to discuss a sponsorship.
## Self-hosting

View File

@@ -1,5 +1,71 @@
# @papra/docs
## 0.6.1
### Patch Changes
- [#512](https://github.com/papra-hq/papra/pull/512) [`cb3ce6b`](https://github.com/papra-hq/papra/commit/cb3ce6b1d8d5dba09cbf0d2964f14b1c93220571) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added organizations permissions for api keys
## 0.6.0
### Minor Changes
- [#480](https://github.com/papra-hq/papra/pull/480) [`0a03f42`](https://github.com/papra-hq/papra/commit/0a03f42231f691d339c7ab5a5916c52385e31bd2) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added documents encryption layer
## 0.5.3
### Patch Changes
- [#455](https://github.com/papra-hq/papra/pull/455) [`b33fde3`](https://github.com/papra-hq/papra/commit/b33fde35d3e8622e31b51aadfe56875d8e48a2ef) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Improved feedback message in case of invalid origin configuration
## 0.5.2
### Patch Changes
- [#405](https://github.com/papra-hq/papra/pull/405) [`3401cfb`](https://github.com/papra-hq/papra/commit/3401cfbfdc7e280d2f0f3166ceddcbf55486f574) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Introduce APP_BASE_URL to mutualize server and client base url
- [#379](https://github.com/papra-hq/papra/pull/379) [`6cedc30`](https://github.com/papra-hq/papra/commit/6cedc30716e320946f79a0a9fd8d3b26e834f4db) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Updated dependencies
- [#390](https://github.com/papra-hq/papra/pull/390) [`42bc3c6`](https://github.com/papra-hq/papra/commit/42bc3c669840eb778d251dcfb0dd96b45bf6e277) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added API endpoints documentation
- [#402](https://github.com/papra-hq/papra/pull/402) [`1d23f40`](https://github.com/papra-hq/papra/commit/1d23f4089479387d5b87dbcf6d3819f5ee14d580) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix invalid domain in json schema urls
## 0.5.1
### Patch Changes
- [#357](https://github.com/papra-hq/papra/pull/357) [`585c53c`](https://github.com/papra-hq/papra/commit/585c53cd9d0d7dbd517dbb1adddfd9e7b70f9fe5) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added a /llms.txt on main website
## 0.5.0
### Minor Changes
- [#337](https://github.com/papra-hq/papra/pull/337) [`1c574b8`](https://github.com/papra-hq/papra/commit/1c574b8305eb7bde4f1b75ac38a610ca0120a613) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added troubleshooting page
### Patch Changes
- [#337](https://github.com/papra-hq/papra/pull/337) [`1c574b8`](https://github.com/papra-hq/papra/commit/1c574b8305eb7bde4f1b75ac38a610ca0120a613) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added `docker compose up` command in dc generator
## 0.4.2
### Patch Changes
- [#322](https://github.com/papra-hq/papra/pull/322) [`f54b8e1`](https://github.com/papra-hq/papra/commit/f54b8e162acd6cfe92241aaa649847fc03ca5852) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Auto computes urls from the provided port
## 0.4.1
### Patch Changes
- [#320](https://github.com/papra-hq/papra/pull/320) [`8ccdb74`](https://github.com/papra-hq/papra/commit/8ccdb748349a3cacf38f032fd4d3beebce202487) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added base url configuration in docker compose generator
## 0.4.0
### Minor Changes
- [#295](https://github.com/papra-hq/papra/pull/295) [`438a311`](https://github.com/papra-hq/papra/commit/438a31171c606138c4b7fa299fdd58dcbeaaf298) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added support for custom oauth2 providers
- [#293](https://github.com/papra-hq/papra/pull/293) [`53bf93f`](https://github.com/papra-hq/papra/commit/53bf93f128b54ad1d3553e18680c87ab23155f8d) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added a papra docker-compose.yml generator
## 0.3.1
### Patch Changes

View File

@@ -3,7 +3,9 @@ import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import starlightLinksValidator from 'starlight-links-validator';
import starlightThemeRapide from 'starlight-theme-rapide';
import UnoCSS from 'unocss/astro';
import { sidebar } from './src/content/navigation';
import posthogRawScript from './src/scripts/posthog.script.js?raw';
const posthogApiKey = env.POSTHOG_API_KEY;
@@ -16,6 +18,7 @@ const posthogScript = posthogRawScript.replace('[POSTHOG-API-KEY]', posthogApiKe
export default defineConfig({
site: 'https://docs.papra.app',
integrations: [
UnoCSS(),
starlight({
plugins: [starlightThemeRapide(), starlightLinksValidator({ exclude: ['http://localhost:1221'] })],
title: 'Papra Docs',
@@ -38,7 +41,7 @@ export default defineConfig({
sidebar,
favicon: '/favicon.svg',
head: [
// Add ICO favicon fallback for Safari.
// Add ICO favicon fallback for Safari.
{
tag: 'link',
attrs: {

1382
apps/docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"name": "@papra/docs",
"type": "module",
"version": "0.3.1",
"version": "0.6.1",
"private": true,
"packageManager": "pnpm@10.9.0",
"packageManager": "pnpm@10.12.3",
"description": "Papra documentation website",
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
"license": "AGPL-3.0-or-later",
@@ -18,11 +18,16 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@astrojs/starlight": "^0.34.2",
"astro": "^5.7.10",
"@astrojs/solid-js": "^5.1.0",
"@astrojs/starlight": "^0.34.3",
"astro": "^5.8.0",
"sharp": "^0.32.5",
"shiki": "^3.4.2",
"starlight-links-validator": "^0.16.0",
"starlight-theme-rapide": "^0.5.0",
"tailwind-merge": "^2.6.0",
"unocss-preset-animations": "^1.2.1",
"yaml": "^2.8.0",
"zod-to-json-schema": "^3.24.5"
},
"devDependencies": {
@@ -32,9 +37,11 @@
"@unocss/reset": "^0.64.0",
"eslint": "^9.17.0",
"eslint-plugin-astro": "^1.3.1",
"figue": "^2.2.2",
"figue": "^3.1.1",
"lodash-es": "^4.17.21",
"marked": "^15.0.6",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"unocss": "0.65.0-beta.2",
"vitest": "catalog:"
}
}

View File

@@ -0,0 +1,3 @@
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,4 +1,38 @@
:root[data-theme='dark'] {
--background: 240 4% 10%;
--foreground: 0 0% 98%;
--card: 240 4% 8%;
--card-foreground: 0 0% 98%;
--popover: 240 4% 8%;
--popover-foreground: 0 0% 98%;
--primary: 77 100% 74%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--warning: 31 98% 50%;
--warning-foreground: 0 0% 98%;
--border: 345 4% 17%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--background-color: #0c0d0f!important;
--accent-color: #fff!important;
--foreground-color: #9ea3a2!important;
@@ -55,4 +89,8 @@
.site-title img {
width: 1.8rem !important;
}
}
pre.shiki {
border-radius: 0.5rem!important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

View File

@@ -0,0 +1,48 @@
const linesToRemove = [
/^# (.*)$/gm, // Remove main title
/^### (.*)$/gm, // Remove section titles
];
export function parseChangelog(changelog: string) {
const logs: { entries: {
pr: { number: number; url: string };
commit: { hash: string; url: string };
contributor: { username: string; url: string };
content: string;
}[]; version: string; }[] = [];
for (const lineToRemove of linesToRemove) {
changelog = changelog.replace(lineToRemove, '');
}
const sections = changelog.match(/## (.*)\n([\s\S]*?)(?=\n## |$)/g) ?? [];
for (const section of sections) {
const version = section.match(/## (.*)\n/)?.[1].trim() ?? 'unknown version';
const entries = section.split('\n- ').slice(1).map((entry) => {
// Example entry:
// [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Maybe multiline content
// Thanks copilot! :sweat-smile:
const prMatch = entry.match(/\[#(\d+)\]\((https:\/\/github\.com\/papra-hq\/papra\/pull\/\d+)\)/);
const commitMatch = entry.match(/\[`([a-f0-9]{7,40})`\]\((https:\/\/github\.com\/papra-hq\/papra\/commit\/[a-f0-9]{7,40})\)/);
const contributorMatch = entry.match(/Thanks \[@([\w-]+)\]\((https:\/\/github\.com\/[\w-]+)\)/);
const contentMatch = entry.match(/\)! - (.*)$/s);
return {
pr: prMatch ? { number: Number.parseInt(prMatch[1], 10), url: prMatch[2] } : { number: 0, url: '' },
commit: commitMatch ? { hash: commitMatch[1], url: commitMatch[2] } : { hash: '', url: '' },
contributor: contributorMatch ? { username: contributorMatch[1], url: contributorMatch[2] } : { username: 'unknown', url: '' },
content: contentMatch ? contentMatch[1].trim() : entry.trim(),
};
});
logs.push({
version,
entries,
});
}
return logs;
}

View File

@@ -0,0 +1,154 @@
---
const iconSize = '20';
const refreshIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/></svg>`;
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z"/><path d="M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1"/></g></svg>`;
const copiedIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path fill="currentColor" d="M18.333 6A3.667 3.667 0 0 1 22 9.667v8.666A3.667 3.667 0 0 1 18.333 22H9.667A3.667 3.667 0 0 1 6 18.333V9.667A3.667 3.667 0 0 1 9.667 6zM15 2c1.094 0 1.828.533 2.374 1.514a1 1 0 1 1-1.748.972C15.405 4.088 15.284 4 15 4H5c-.548 0-1 .452-1 1v9.998c0 .32.154.618.407.805l.1.065a1 1 0 1 1-.99 1.738A3 3 0 0 1 2 15V5c0-1.652 1.348-3 3-3zm1.293 9.293L13 14.585l-1.293-1.292a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414"/></svg>`;
---
<div class="key-generator">
<div class="key-row">
<input type="text" class="key-input" readonly />
<button class="cbtn btn-refresh" title="Generate new key">
<span set:html={refreshIcon} aria-label="Refresh" />
</button>
<button class="cbtn btn-copy" title="Copy to clipboard">
<span set:html={copyIcon} aria-label="Copy" class="icon-copy" />
<span set:html={copiedIcon} aria-label="Copied" class="icon-copied hidden" />
</button>
</div>
<div class="info-text">
Generated locally in your browser - no network or server involved
</div>
</div>
<script>
function generateKey({ keyInputElement }: { keyInputElement: HTMLInputElement }) {
// Generate a 32-byte (256-bit) encryption key
const array = new Uint8Array(32);
crypto.getRandomValues(array);
// Convert to hex format
const key = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
keyInputElement.value = key;
}
function copyToClipboard({ keyInputElement, copyButtonElement, iconCopyElement, iconCopiedElement }: { keyInputElement: HTMLInputElement; copyButtonElement: HTMLButtonElement; iconCopyElement: HTMLSpanElement; iconCopiedElement: HTMLSpanElement }) {
keyInputElement.select();
keyInputElement.setSelectionRange(0, 64); // For mobile devices
navigator.clipboard.writeText(keyInputElement.value).then(() => {
iconCopyElement.classList.add('hidden');
iconCopiedElement.classList.remove('hidden');
copyButtonElement.disabled = true;
setTimeout(() => {
iconCopyElement.classList.remove('hidden');
iconCopiedElement.classList.add('hidden');
copyButtonElement.disabled = false;
}, 1_000);
}).catch(() => {
// Fallback for older browsers
document.execCommand('copy');
});
}
const keyGenerators = document.querySelectorAll('.key-generator');
keyGenerators.forEach((keyGenerator) => {
const refreshButtonElement = keyGenerator.querySelector('.btn-refresh')!;
const copyButtonElement = keyGenerator.querySelector<HTMLButtonElement>('.btn-copy')!;
const keyInputElement = keyGenerator.querySelector<HTMLInputElement>('.key-input')!;
const iconCopyElement = keyGenerator.querySelector<HTMLSpanElement>('.icon-copy')!;
const iconCopiedElement = keyGenerator.querySelector<HTMLSpanElement>('.icon-copied')!;
generateKey({ keyInputElement });
refreshButtonElement.addEventListener('click', () => generateKey({ keyInputElement }));
copyButtonElement.addEventListener('click', () => copyToClipboard({ copyButtonElement, keyInputElement, iconCopyElement, iconCopiedElement }));
});
</script>
<style>
.key-generator {
/* background-color: var(--ec-frm-trmBg);
border-radius: var(--ec-brdRad);
border: 1px solid var(--ec-brdCol);
font-family: monospace;
max-width: 100%; */
}
.key-row {
display: flex;
align-items: center;
}
.key-input {
flex: 1;
background-color: var(--sl-color-black);
border: 1px solid var(--sl-color-gray-5);
border-radius: 4px 0 0 4px;
padding: 8px 12px;
font-family: var(--__sl-font-mono, monospace);
font-size: 14px;
color: var(--sl-color-gray-2);
min-width: 0; /* Allow input to shrink */
border-right: none;
}
.key-input:focus {
outline: none;
border-color: var(--ec-frm-inpBrd, #4a9eff);
box-shadow: 0 0 0 2px var(--ec-frm-inpBrd, #4a9eff)33;
}
.cbtn {
background-color: var(--ec-frm-btnBg);
border: 1px solid var(--ec-brdCol);
padding: 10px 12px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 0;
}
.cbtn.btn-copy {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-left: none;
}
.cbtn.btn-refresh {
border-radius: 0;
}
.cbtn:hover {
background-color: var(--sl-color-gray-6)!important;
}
.cbtn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-refresh:hover:not(:disabled) {
background-color: var(--ec-frm-btnBgHover, #3a3a3a);
}
.btn-copy:hover:not(:disabled) {
background-color: var(--ec-frm-btnBgHover, #3a3a3a);
}
.info-text {
color: var(--ec-frm-txtSecondary, #888888);
font-style: italic;
margin-top: 0;
}
</style>

View File

@@ -1,6 +1,8 @@
import type { ConfigDefinition, ConfigDefinitionElement } from 'figue';
import { isArray, isEmpty, isNil } from 'lodash-es';
import { castArray, isArray, isEmpty, isNil } from 'lodash-es';
import { configDefinition } from '../../papra-server/src/modules/config/config';
import { renderMarkdown } from './markdown';
function walk(configDefinition: ConfigDefinition, path: string[] = []): (ConfigDefinitionElement & { path: string[] })[] {
return Object
@@ -33,24 +35,32 @@ const rows = configDetails
const rawDocumentation = formatDoc(doc);
// The client baseUrl default value is overridden in the Dockerfiles
const defaultOverride = path.join('.') === 'client.baseUrl' ? 'http://localhost:1221' : undefined;
return {
path,
env,
documentation: rawDocumentation,
defaultValue: isEmptyDefaultValue ? undefined : defaultValue,
defaultValue: defaultOverride ?? (isEmptyDefaultValue ? undefined : defaultValue),
};
});
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => `
### ${env}
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => {
const envs = castArray(env);
const [firstEnv, ...restEnvs] = envs;
return `
### ${firstEnv}
${documentation}
- Path: \`${path.join('.')}\`
- Environment variable: \`${env}\`
- Environment variable: \`${firstEnv}\` ${restEnvs.length ? `, with fallback to: ${restEnvs.map(e => `\`${e}\``).join(', ')}` : ''}
- Default value: \`${defaultValue}\`
`.trim()).join('\n\n---\n\n');
`.trim();
}).join('\n\n---\n\n');
function wrapText(text: string, maxLength = 75) {
const words = text.split(' ');
@@ -75,11 +85,15 @@ function wrapText(text: string, maxLength = 75) {
const fullDotEnv = rows.map(({ env, defaultValue, documentation }) => {
const isEmptyDefaultValue = isNil(defaultValue) || (isArray(defaultValue) && isEmpty(defaultValue)) || defaultValue === '';
const envs = castArray(env);
const [firstEnv] = envs;
return [
...wrapText(documentation),
`# ${env}=${isEmptyDefaultValue ? '' : defaultValue}`,
`# ${firstEnv}=${isEmptyDefaultValue ? '' : defaultValue}`,
].join('\n');
}).join('\n\n');
export { fullDotEnv, mdSections };
const sectionsHtml = renderMarkdown(mdSections);
export { fullDotEnv, mdSections, sectionsHtml };

View File

@@ -31,6 +31,7 @@ Launch Papra with default configuration using:
docker run -d \
--name papra \
--restart unless-stopped \
--env APP_BASE_URL=http://localhost:1221 \
-p 1221:1221 \
ghcr.io/papra-hq/papra:latest
```
@@ -69,6 +70,7 @@ For production deployments, mount host directories to preserve application data
docker run -d \
--name papra \
--restart unless-stopped \
--env APP_BASE_URL=http://localhost:1221 \
-p 1221:1221 \
-v $(pwd)/papra-data:/app/app-data \
--user $(id -u):$(id -g) \

View File

@@ -1,6 +1,7 @@
---
title: Using Docker Compose
slug: self-hosting/using-docker-compose
description: Self-host Papra using Docker Compose.
---
import { Steps } from '@astrojs/starlight/components';

View File

@@ -1,17 +1,29 @@
---
title: Configuration
slug: self-hosting/configuration
description: Configure your self-hosted Papra instance.
---
import { mdSections, fullDotEnv } from '../../../config.data.ts';
import { marked } from 'marked';
import { sectionsHtml, fullDotEnv } from '../../../config.data.ts';
import { Tabs, TabItem } from '@astrojs/starlight/components';
import { Aside } from '@astrojs/starlight/components';
import { Code } from '@astrojs/starlight/components';
Configuring your self hosted Papra allows you to customize the application to better suit your environment and requirements. This guide covers the key environment variables you can set to control various aspects of the application, including port settings, security options, and storage configurations.
## Complete .env
Here is the full configuration file that you can use to configure Papra. The variables values are the default values.
<Code code={fullDotEnv} language="env" title=".env" />
## Configuration variables
Here is the complete list of configuration variables that you can use to configure Papra. You can set these variables in the `.env` file or pass them as environment variables when running the Docker container.
<Fragment set:html={sectionsHtml} />
## Configuration files
You can configure Papra using standard environment variables or use some configuration files.
@@ -42,7 +54,7 @@ Example of configuration files:
<TabItem label="papra.config.json">
```json
{
"$schema": "https://docs.papra.com/papra-config-schema.json",
"$schema": "https://docs.papra.app/papra-config-schema.json",
"server": {
"baseUrl": "https://papra.example.com"
},
@@ -61,7 +73,7 @@ Example of configuration files:
```json
{
"$schema": "https://docs.papra.com/papra-config-schema.json",
"$schema": "https://docs.papra.app/papra-config-schema.json",
// ...
}
```
@@ -72,17 +84,4 @@ Example of configuration files:
</Tabs>
You'll find the complete list of configuration variables with their environment variables equivalents and path for files in the next section.
## Complete .env
Here is the full configuration file that you can use to configure Papra. The variables values are the default values.
<Code code={fullDotEnv} language="env" title=".env" />
## Configuration variables
Here is the complete list of configuration variables that you can use to configure Papra. You can set these variables in the `.env` file or pass them as environment variables when running the Docker container.
<Fragment set:html={marked.parse(mdSections)} />
You'll find the complete list of configuration variables with their environment variables equivalents and path for files in the previous section.

View File

@@ -39,7 +39,7 @@ By integrating Papra with OwlRelay, your instance will generate email addresses
3. **Configure your Papra instance**
Once you have created your API key, you can configure your Papra instance to receive emails by setting the `OWLRELAY_API_KEY` and `OWLRELAY_WEBHOOK_SECRET` environment variables.
Once you have created your API key, you can configure your Papra instance to receive emails by setting the `OWLRELAY_API_KEY` and `INTAKE_EMAILS_WEBHOOK_SECRET` environment variables.
```bash
# Enable intake emails

View File

@@ -0,0 +1,181 @@
---
title: Setup Custom OAuth2 Providers
description: Step-by-step guide to setup custom OAuth2 providers for authentication in your Papra instance.
slug: guides/setup-custom-oauth2-providers
---
import { Aside } from '@astrojs/starlight/components';
import { Steps } from '@astrojs/starlight/components';
This guide will show you how to configure custom OAuth2 providers for authentication in your Papra instance.
<Aside type="note">
Papra's OAuth2 implementation is based on the [Better Auth Generic OAuth plugin](https://www.better-auth.com/docs/plugins/generic-oauth). For more detailed information about the configuration options and advanced usage, please refer to their documentation.
</Aside>
## Prerequisites
In order to follow this guide, you need:
- A custom OAuth2 provider
- An accessible Papra instance
- Basic understanding of OAuth2 flows
## Configuration
To set up custom OAuth2 providers, you'll need to configure the `AUTH_PROVIDERS_CUSTOMS` environment variable with an array of provider configurations. Here's an example:
```bash
AUTH_PROVIDERS_CUSTOMS='[
{
"providerId": "custom-oauth2",
"providerName": "Custom OAuth2",
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"type": "oidc",
"discoveryUrl": "https://your-provider.tld/.well-known/openid-configuration",
"scopes": ["openid", "profile", "email"]
}
]'
```
Each provider configuration supports the following fields:
- `providerId`: A unique identifier for the OAuth provider
- `providerName`: The display name of the provider
- `providerIconUrl`: URL of the icon to display (optional) you can use a base64 encoded image or an url to a remote image.
- `clientId`: OAuth client ID
- `clientSecret`: OAuth client secret
- `type`: Type of OAuth flow ("oauth2" or "oidc")
- `discoveryUrl`: URL to fetch OAuth 2.0 configuration (recommended for OIDC providers)
- `authorizationUrl`: URL for the authorization endpoint (required for OAuth2 if not using discoveryUrl)
- `tokenUrl`: URL for the token endpoint (required for OAuth2 if not using discoveryUrl)
- `userInfoUrl`: URL for the user info endpoint (required for OAuth2 if not using discoveryUrl)
- `scopes`: Array of OAuth scopes to request
- `redirectURI`: Custom redirect URI (optional)
- `responseType`: OAuth response type (defaults to "code")
- `prompt`: Controls the authentication experience ("select_account", "consent", "login", "none")
- `pkce`: Whether to use PKCE (Proof Key for Code Exchange)
- `accessType`: Access type for the authorization request
## Setup
<Steps>
1. **Configure your OAuth2 Provider**
First, you'll need to register your application with your OAuth2 provider. This typically involves:
- Creating a new application in your provider's dashboard
- Setting up the redirect URI (usually `https://<your-papra-instance>/api/auth/oauth2/callback/:providerId`)
- Obtaining the client ID and client secret
- Configuring the required scopes
2. **Configure Papra**
Add the `AUTH_PROVIDERS_CUSTOMS` environment variable to your Papra instance. Here are some examples:
For OIDC providers:
```json
[
{
"providerId": "custom-oauth2",
"providerName": "Custom OAuth2",
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"type": "oidc",
"discoveryUrl": "https://your-provider.tld/.well-known/openid-configuration",
"scopes": ["openid", "profile", "email"]
}
]
```
For standard OAuth2 providers:
```json
[
{
"providerId": "custom-oauth2",
"providerName": "Custom OAuth2",
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"type": "oauth2",
"authorizationUrl": "https://your-provider.tld/oauth2/authorize",
"tokenUrl": "https://your-provider.tld/oauth2/token",
"userInfoUrl": "https://your-provider.tld/oauth2/userinfo",
"scopes": ["profile", "email"]
}
]
```
<Aside type="note">
The `discoveryUrl` is recommended for OIDC providers as it automatically configures all the necessary endpoints.
For standard OAuth2 providers, you'll need to specify the endpoints manually.
</Aside>
3. **Test the Configuration**
- Restart your Papra instance to apply the changes
- Go to the login page
- You should see your custom OAuth2 providers as login options
- Try logging in with a test account
</Steps>
## Troubleshooting
### Providers Not Showing Up
If your OAuth2 providers are not showing up on the login page:
- Check that the JSON configuration in `AUTH_PROVIDERS_CUSTOMS` is valid
- Ensure all required fields are provided
- Verify that the provider IDs are unique
### Authentication Fails
If authentication fails:
- Verify that the redirect URI is correctly configured in your OAuth2 provider
- Check that the client ID and client secret are correct
- Ensure the required scopes are properly configured
- Check the Papra logs for any error messages
### OIDC Discovery Issues
If you're using OIDC and experiencing issues:
- Verify that the `discoveryUrl` is accessible
- Check that the provider supports OIDC discovery
- Ensure the provider's configuration is properly exposed through the discovery endpoint
## Security Considerations
<Aside type="caution">
Always use HTTPS for your OAuth2 endpoints and ensure your client secret is kept secure.
Consider using PKCE (Proof Key for Code Exchange) for additional security by setting `pkce: true` in your configuration.
</Aside>
## Multiple Providers
You can configure multiple custom OAuth2 providers by adding them to the array:
```json
[
{
"providerId": "custom-oauth2-1",
"providerName": "Custom OAuth2 Provider 1",
"type": "oidc",
"discoveryUrl": "https://provider1.tld/.well-known/openid-configuration",
"clientId": "client-id-1",
"clientSecret": "client-secret-1",
"scopes": ["openid", "profile", "email"]
},
{
"providerId": "custom-oauth2-2",
"providerName": "Custom OAuth2 Provider 2",
"type": "oidc",
"discoveryUrl": "https://provider2.tld/.well-known/openid-configuration",
"clientId": "client-id-2",
"clientSecret": "client-secret-2",
"scopes": ["openid", "profile", "email"]
}
]
```

View File

@@ -0,0 +1,448 @@
---
title: Setup Document Encryption
description: Step-by-step guide to enable and configure document encryption in Papra for enhanced data security.
slug: guides/document-encryption
---
import { Steps } from '@astrojs/starlight/components';
import { Aside } from '@astrojs/starlight/components';
import { Code } from '@astrojs/starlight/components';
import { Tabs, TabItem } from '@astrojs/starlight/components';
import EncryptionKeyGenerator from '../../../components/encryption-key-generator.astro';
<Aside type="note">
Document encryption is available in Papra v0.9.0 and above.
</Aside>
Document encryption in Papra provides end-to-end protection for your stored documents using industry-standard AES-256-GCM encryption. This guide will walk you through enabling encryption, understanding how it works, and managing encryption keys.
## How Encryption Works
Papra uses a two-layer encryption approach that provides both security and flexibility:
### Key Encryption Architecture
1. **Key Encryption Key (KEK)**: A master key that you provide, used to encrypt document-specific keys
2. **Document Encryption Key (DEK)**: Unique per-document keys that actually encrypt your files
3. **File Encryption**: Each document gets its own random 256-bit encryption key for maximum security
<div class="dark:block hidden">
![Key Encryption Architecture](../../../assets/docs/encryption-schema-light.png)
</div>
<div class="dark:hidden block">
![Key Encryption Architecture](../../../assets/docs/encryption-schema-dark.png)
</div>
### Encryption Flow
<Steps>
1. **Document Upload**: When you upload a document, Papra generates a unique 256-bit encryption key (DEK)
2. **File Encryption**: The document is encrypted using AES-256-GCM with the DEK
3. **Key Wrapping**: The DEK is encrypted (wrapped) using your Key Encryption Key (KEK)
4. **Storage**: The encrypted document and wrapped DEK are stored separately - the file in your storage backend, the wrapped key in the database along with the document metadata
5. **Retrieval**: When accessing a document, Papra unwraps the DEK using your KEK, then decrypts the file stream
</Steps>
<Aside type="note">
This architecture means that even if someone gains access to your file storage, they cannot decrypt documents without access to both your document records and your KEK, in other words, without your database and your environment variables.
</Aside>
## Quick Setup
<Steps>
1. **Generate an encryption key**
Generate a secure random 256-bit key in hex format, using this generator or OpenSSL command.
<Tabs>
<TabItem label="Key generator">
<EncryptionKeyGenerator />
</TabItem>
<TabItem label="OpenSSL command">
```bash
openssl rand -hex 32
```
This will output something like: `0deba5534bd70548de92d1fd4ae37cf901cca3dc20589b7e022ddb680c98e50c`
</TabItem>
</Tabs>
2. **Enable encryption in your configuration**
Add the following environment variables to your `.env` file or Docker configuration:
```bash
DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-encryption-key>
```
3. **Restart Papra**
Restart your Papra instance to apply the encryption settings.
</Steps>
## Configuration Options
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED` | Enable/disable document encryption | No |
| `DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS` | Key encryption keys for document encryption | Yes (if encryption enabled) |
### Key Formats
<Tabs>
<TabItem label="Single Key">
For simple setups, provide a single 32-byte hex string:
```bash
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-encryption-key>
```
This key will automatically be assigned version `1`.
</TabItem>
<TabItem label="Multiple Keys">
For key rotation and advanced setups, provide versioned keys:
```bash
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=1:<your-encryption-key-1>,2:<your-encryption-key-2>
```
- The highest version key encrypts new documents
- All keys can decrypt existing documents
- Versions can be any alphabetically sortable string
- Order in the list doesn't matter
</TabItem>
</Tabs>
## Docker Compose Setup
Add encryption configuration to your Docker Compose file:
<Tabs>
<TabItem label="Environment Variables">
```yaml title="docker-compose.yml" ins={8-9}
services:
papra:
container_name: papra
image: ghcr.io/papra-hq/papra:latest
restart: unless-stopped
environment:
# ... other environment variables ...
- DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true
- DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-encryption-key>
volumes:
- ./app-data:/app/app-data
ports:
- "1221:1221"
```
</TabItem>
<TabItem label="Config File">
```yaml title="docker-compose.yml"
services:
papra:
container_name: papra
image: ghcr.io/papra-hq/papra:latest
restart: unless-stopped
volumes:
- ./app-data:/app/app-data
- ./papra.config.yaml:/app/app-data/papra.config.yaml
ports:
- "1221:1221"
```
```yaml title="./papra.config.yaml"
documentsStorage:
encryption:
isEncryptionEnabled: true
documentKeyEncryptionKeys: "<your-encryption-key>"
```
</TabItem>
</Tabs>
## Key Management
### Key Rotation
Key rotation allows you to replace encryption keys without losing access to existing documents:
<Steps>
1. **Generate a new key**
```bash
openssl rand -hex 32
```
2. **Add the new key with a higher version**
```bash
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=1:old_key_here,2:new_key_here
```
3. **Restart Papra**
New documents will use the highest version key (version 2), while existing documents remain accessible with the old key.
4. **Optional: Remove old keys**
Once you're confident all documents are using the new key, you can remove old keys. However, this will make any documents encrypted with old keys inaccessible.
</Steps>
<Aside type="caution">
Never remove a key version if there are still documents encrypted with that key, unless you're certain you no longer need access to those documents.
</Aside>
### Key Security Best Practices
1. **Store keys securely**: Use a secrets management system in production
2. **Use different keys per environment**: Development, staging, and production should have separate keys
3. **Backup your keys**: Loss of encryption keys means permanent loss of document access
4. **Rotate keys periodically**: Consider rotating keys annually or after security incidents
5. **Limit key access**: Only authorized personnel should have access to encryption keys
### Docker Secrets Example
For production environments, store your encryption keys securely using external secret management systems or secure file systems, and reference them via environment variables.
## Compatibility and Migration
### Enabling Encryption on Existing Instances
When you enable encryption on a Papra instance that already has documents:
- **Existing documents**: Remain unencrypted but accessible
- **New documents**: Are encrypted using the current KEK
- **Mixed storage**: Papra automatically handles both encrypted and unencrypted documents
### Migrating Existing Documents to Encrypted Format
If you want to encrypt all existing unencrypted documents after enabling encryption, Papra provides a maintenance command to handle this migration automatically.
<Aside type="caution">
It's advised to make a backup of your documents and database before running the migration.
</Aside>
<Steps>
1. **Verify encryption is properly configured**
Ensure encryption is enabled and working for new documents before migrating existing ones:
```bash
# Check that your configuration includes:
DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true
DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS=<your-key>
```
2. **Run dry-run to preview changes**
<Tabs>
<TabItem label="Docker Compose">
```bash
# Run dry-run inside the Docker container
docker compose exec papra pnpm maintenance:encrypt-all-documents --dry-run
```
</TabItem>
<TabItem label="Docker">
```bash
# Run dry-run inside the Docker container
docker exec -it papra pnpm maintenance:encrypt-all-documents --dry-run
```
</TabItem>
<TabItem label="Source Installation">
```bash
# From your Papra server directory
pnpm maintenance:encrypt-all-documents --dry-run
```
</TabItem>
</Tabs>
This will show you:
- How many documents will be encrypted
- Which documents will be affected
- No actual encryption will be performed
4. **Run the migration**
<Tabs>
<TabItem label="Docker Compose">
```bash
# Run migration inside the Docker container
docker compose exec papra pnpm maintenance:encrypt-all-documents
```
</TabItem>
<TabItem label="Docker">
```bash
# Run migration inside the Docker container
docker exec -it papra pnpm maintenance:encrypt-all-documents
```
</TabItem>
<TabItem label="Source Installation">
```bash
# From your Papra server directory
pnpm maintenance:encrypt-all-documents
```
</TabItem>
</Tabs>
The command will:
- Find all unencrypted documents
- Encrypt each document using your configured KEK
- Update database records with encryption metadata
- Remove original unencrypted files from storage
- Provide progress logging throughout the process
5. **Verify migration success**
After migration:
- Test document access through the Papra interface
- Check that storage files are now encrypted (should start with `PP01`)
- Verify all documents are accessible and downloadable
</Steps>
<Aside type="tip">
**Migration Performance**
- The migration processes documents sequentially to ensure reliability
- Large document collections may take considerable time
- Monitor disk space during migration (temporary storage overhead)
- Consider running during maintenance windows for production systems
</Aside>
#### Troubleshooting Migration Issues
**Migration fails with "Document encryption is not enabled"**
- Verify `DOCUMENT_STORAGE_ENCRYPTION_IS_ENABLED=true` is set
- Restart Papra after configuration changes
**Migration fails with "Document encryption keys are not set"**
- Ensure `DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS` contains valid keys
- Verify key format is correct (64-character hex string)
**Migration stops or fails partway**
- Check available disk space
- Review Papra logs for specific error messages
- Restore from backup and retry after fixing the issue
**Documents inaccessible after migration**
- Verify encryption keys are still properly configured
- Check that Papra can access your storage backend
- Restore from backup if necessary
### Disabling Encryption
If you disable encryption:
- **Encrypted documents**: Remain encrypted but are automatically decrypted when accessed (if KEK is still available)
- **New documents**: Are stored unencrypted
- **Data loss risk**: If you remove the KEK while encrypted documents exist, those documents become inaccessible
<Aside type="caution">
Disabling encryption doesn't automatically decrypt existing documents in storage. They remain encrypted and require the KEK for access.
</Aside>
### Storage Driver Compatibility
The encryption layer sits between Papra and your chosen storage driver, providing consistent encryption regardless of where files are stored (S3, Azure Blob Storage, File System, etc.).
## Technical Details
### Encryption Algorithm
- **Algorithm**: AES-256-GCM (Authenticated Encryption)
- **Key size**: 256 bits (32 bytes)
- **IV size**: 96 bits (12 bytes)
- **Authentication tag**: 128 bits (16 bytes)
### File Format
Encrypted files use a custom format with a magic number for identification:
```
| Magic (4 bytes) | IV (12 bytes) | Encrypted Data | Auth Tag (16 bytes) |
```
- **Magic number**: `PP01` - identifies Papra encrypted files
- **IV**: Initialization vector for GCM mode
- **Encrypted Data**: The actual encrypted document content
- **Auth Tag**: Authentication tag for integrity verification
### Performance Considerations
- **Streaming encryption**: Files are encrypted/decrypted in streams, minimizing memory usage
- **No size overhead**: Minimal storage overhead (32 bytes per file for headers)
- **CPU impact**: Modern processors handle AES encryption efficiently
## Troubleshooting
### Common Issues
**"Document KEK required" error**
- Ensure `DOCUMENT_STORAGE_DOCUMENT_KEY_ENCRYPTION_KEYS` is set
- Verify the key format is correct (64 character hex string)
**"Document KEK not found" error**
- The document was encrypted with a key version that's no longer available
- Add the missing key version back to your configuration
**"Unsupported encryption algorithm" error**
- The document uses an encryption algorithm not supported by this Papra version
- This shouldn't occur in normal operation
**Performance issues**
- Consider your storage driver's performance characteristics
- Encryption adds minimal overhead, but network/disk I/O remains the bottleneck
### Verification
To verify encryption is working:
1. Upload a document after enabling encryption
2. Check your storage backend - the file should not be readable as plain text
3. The file should start with the magic number `PP01` if you examine it directly
<Aside>
You can find complete configuration options in the [configuration reference](/self-hosting/configuration). Look for variables prefixed with `DOCUMENT_STORAGE_ENCRYPTION_`.
</Aside>
## Security Considerations
### Threat Model
Document encryption in Papra protects against:
- **Storage compromise**: If your file storage is breached, documents remain encrypted
- **Database-only breach**: Without the KEK, wrapped DEKs cannot be unwrapped
- **Configuration exposure**: If the KEK is exposed, the files remain encrypted as long as the DEK are not exposed
### Limitations
Encryption does not protect against:
- **Application-level access**: Users with document access can view decrypted content
- **Memory dumps**: Decrypted content exists temporarily in application memory
- **Key and database compromise**: If KEKs are stolen, all DEKs can be decrypted if the database is compromised
- **Full system compromise**: If the entire Papra instance is compromised, documents can be accessed

View File

@@ -0,0 +1,40 @@
---
title: Troubleshooting
description: Troubleshooting guide for Papra
slug: resources/troubleshooting
---
You can find here some common issues and how to fix them. If you encounter an issue that is not listed here, please [open an issue](https://github.com/papra-hq/papra/issues/new/choose) or [join our Discord](https://papra.app/discord).
## Failed to ensure that the database directory exists
Upon starting the server or a script, you may encounter this error
```
Failed to ensure that the database directory exists, error while creating the directory
Error: EACCES: permission denied, mkdir './app-data/db'
```
Before accessing the DB sqlite file, the server will try to ensure that the database directory exists, and if it doesn't, it try will create it. But in case of insufficient permissions, it will fail.
To fix this, you can either:
- Create the directory manually `mkdir -p <your-app-data-dir>/db`
- Ensure that the directory is owned by the user running the container
- Run the server as root (not recommended)
## Invalid application origin
Papra ensures [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by validating the Origin header in requests. This check ensures that requests originate from the application or a trusted source. Any request that does not originate from a trusted origin will be rejected.
If you are self-hosting Papra, you may encounter an error stating that the application origin is invalid while trying to login or register.
To fix this, you can either:
- Update the `APP_BASE_URL` environment variable to match the url of your application (e.g. `https://papra.my-homelab.tld`)
- Add the current url to the `TRUSTED_ORIGINS` environment variable if you need to allow multiple origins, comma separated. By default the `TRUSTED_ORIGINS` is set to the `APP_BASE_URL`
- If you are using a reverse proxy, you may need to add the url to the `TRUSTED_ORIGINS` environment variable

View File

@@ -0,0 +1,309 @@
---
title: API Endpoints
description: The list and details of all the API endpoints available in Papra.
slug: resources/api-endpoints
---
## Authentication
The public API uses a bearer token for authentication. You can get a token by logging to your Papra account and creating an API token.
<details>
<summary>How to create an API token</summary>
![API Token](../../../assets/api-key-creation-1.png)
![API Token](../../../assets/api-key-creation-2.png)
</details>
To authenticate your requests, include the token in the `Authorization` header with the `Bearer` prefix:
```
Authorization: Bearer YOUR_API_TOKEN
```
### Examples
**Using cURL:**
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.papra.app/api/organizations
```
**Using JavaScript (fetch):**
```javascript
const response = await fetch('https://api.papra.app/api/organizations', {
headers: {
'Authorization': 'Bearer YOUR_API_TOKEN',
'Content-Type': 'application/json'
}
})
```
### API Key Permissions
When creating an API key, you can select from the following permissions:
**Organizations:**
- `organizations:create` - Create new organizations
- `organizations:read` - Read organization information and list organizations of the user
- `organizations:update` - Update organization details
- `organizations:delete` - Delete organizations
**Documents:**
- `documents:create` - Upload and create new documents
- `documents:read` - Read and download documents
- `documents:update` - Update document metadata and content
- `documents:delete` - Delete documents
**Tags:**
- `tags:create` - Create new tags
- `tags:read` - Read tag information and list tags
- `tags:update` - Update tag details
- `tags:delete` - Delete tags
## Endpoints
### List organizations
**GET** `/api/organizations`
List all organizations accessible to the authenticated user.
- Required API key permissions: `organizations:read`
- Response (JSON)
- `organizations`: The list of organizations.
### Create an organization
**POST** `/api/organizations`
Create a new organization.
- Required API key permissions: `organizations:create`
- Body (JSON)
- `name`: The organization name (3-50 characters).
- Response (JSON)
- `organization`: The created organization.
### Get an organization
**GET** `/api/organizations/:organizationId`
Get an organization by its ID.
- Required API key permissions: `organizations:read`
- Response (JSON)
- `organization`: The organization.
### Update an organization
**PUT** `/api/organizations/:organizationId`
Update an organization's name.
- Required API key permissions: `organizations:update`
- Body (JSON)
- `name`: The new organization name (3-50 characters).
- Response (JSON)
- `organization`: The updated organization.
### Delete an organization
**DELETE** `/api/organizations/:organizationId`
Delete an organization by its ID.
- Required API key permissions: `organizations:delete`
- Response: empty (204 status code)
### Create a document
**POST** `/api/organizations/:organizationId/documents`
Create a new document in the organization.
- Required API key permissions: `documents:create`
- Body (form-data)
- `file`: The file to upload.
- `ocrLanguages`: (optional) The languages to use for OCR.
- Response (JSON)
- `document`: The created document.
### List documents
**GET** `/api/organizations/:organizationId/documents`
List all documents in the organization.
- Required API key permissions: `documents:read`
- Query parameters
- `pageIndex`: (optional, default: 0) The page index to start from.
- `pageSize`: (optional, default: 100) The number of documents to return.
- `tags`: (optional) The tags IDs to filter by.
- Response (JSON)
- `documents`: The list of documents.
- `documentsCount`: The total number of documents.
### List deleted documents (trash)
**GET** `/api/organizations/:organizationId/documents/deleted`
List all deleted documents (in trash) in the organization.
- Required API key permissions: `documents:read`
- Query parameters
- `pageIndex`: (optional, default: 0) The page index to start from.
- `pageSize`: (optional, default: 100) The number of documents to return.
- Response (JSON)
- `documents`: The list of deleted documents.
- `documentsCount`: The total number of deleted documents.
### Get a document
**GET** `/api/organizations/:organizationId/documents/:documentId`
Get a document by its ID.
- Required API key permissions: `documents:read`
- Response (JSON)
- `document`: The document.
### Delete a document
**DELETE** `/api/organizations/:organizationId/documents/:documentId`
Delete a document by its ID.
- Required API key permissions: `documents:delete`
- Response: empty (204 status code)
### Get a document file
**GET** `/api/organizations/:organizationId/documents/:documentId/file`
Get a document file content by its ID.
- Required API key permissions: `documents:read`
- Response: The document file stream.
### Search documents
**GET** `/api/organizations/:organizationId/documents/search`
Search documents in the organization by name or content.
- Required API key permissions: `documents:read`
- Query parameters
- `searchQuery`: The search query.
- `pageIndex`: (optional, default: 0) The page index to start from.
- `pageSize`: (optional, default: 100) The number of documents to return.
- Response (JSON)
- `documents`: The list of documents.
### Get organization documents statistics
**GET** `/api/organizations/:organizationId/documents/statistics`
Get the statistics (number of documents and total size) of the documents in the organization.
- Required API key permissions: `documents:read`
- Response (JSON)
- `organizationStats`: The organization documents statistics.
- `documentsCount`: The total number of documents.
- `documentsSize`: The total size of the documents.
### Update a document
**PATCH** `/api/organizations/:organizationId/documents/:documentId`
Change the name or content (for search purposes) of a document.
- Required API key permissions: `documents:update`
- Body (form-data)
- `name`: (optional) The document name.
- `content`: (optional) The document content.
- Response (JSON)
- `document`: The updated document.
### Get document activity
**GET** `/api/organizations/:organizationId/documents/:documentId/activity`
Get the activity log of a document.
- Required API key permissions: `documents:read`
- Query parameters
- `pageIndex`: (optional, default: 0) The page index to start from.
- `pageSize`: (optional, default: 100) The number of documents to return.
- Response (JSON)
- `activities`: The list of activities.
### Create a tag
**POST** `/api/organizations/:organizationId/tags`
Create a new tag in the organization.
- Required API key permissions: `tags:create`
- Body (form-data)
- `name`: The tag name.
- `color`: The tag color in hex format (e.g. `#000000`).
- `description`: (optional) The tag description.
- Response (JSON)
- `tag`: The created tag.
### List tags
**GET** `/api/organizations/:organizationId/tags`
List all tags in the organization.
- Required API key permissions: `tags:read`
- Response (JSON)
- `tags`: The list of tags.
### Update a tag
**PUT** `/api/organizations/:organizationId/tags/:tagId`
Change the name, color or description of a tag.
- Required API key permissions: `tags:update`
- Body
- `name`: (optional) The tag name.
- `color`: (optional) The tag color in hex format (e.g. `#000000`).
- `description`: (optional) The tag description.
- Response (JSON)
- `tag`: The updated tag.
### Delete a tag
**DELETE** `/api/organizations/:organizationId/tags/:tagId`
Delete a tag by its ID.
- Required API key permissions: `tags:delete`
- Response: empty (204 status code)
### Add a tag to a document
**POST** `/api/organizations/:organizationId/documents/:documentId/tags`
Associate a tag to a document.
- Required API key permissions: `tags:read` and `documents:update`
- Body
- `tagId`: The tag ID.
- Response: empty (204 status code)
### Remove a tag from a document
**DELETE** `/api/organizations/:organizationId/documents/:documentId/tags/:tagId`
Remove a tag from a document.
- Required API key permissions: `tags:read` and `documents:update`
- Response: empty (204 status code)

View File

@@ -0,0 +1,40 @@
---
title: Document Deduplication
description: How Papra prevents duplicate documents and saves storage space.
slug: architecture/document-deduplication
---
## Overview
Papra automatically detects and prevents duplicate documents per organization using content hashing. This ensures that if the same file is uploaded multiple times, only one copy is stored, saving storage space and reducing clutter.
## How It Works
When a document is added to an organization (upload, email ingestion, folder sync, ...), the server computes a **SHA-256 hash** of the file content and checks if a document with the same hash already exists in that organization.
- If there is **no document with the same hash** in the organization, the new document is added as usual
- If a document **with same content exists**, the upload is rejected
- If a document **with same content was previously deleted** (in trash), it is restored instead of creating a new copy, the metadata is updated to match the newly added document
## Technical Details
### Hash Algorithm
- Papra uses **SHA-256** for content hashing.
- Computed during streaming upload (no extra I/O)
- 64-character hexadecimal string stored in the database
### Database Constraint
The database enforces uniqueness with a composite index:
```sql
UNIQUE (organization_id, original_sha256_hash)
```
This guarantees no two active documents in the same organization can have identical content.
### File Content Only
Only the **file content** is hashed and used for deduplication, filenames, upload dates, and metadata don't affect deduplication. Two files are considered duplicates if and only if their content is strictly identical.

View File

@@ -0,0 +1,38 @@
---
title: No-Mutation Principle
description: Why Papra never modifies your original documents and the architectural decisions behind this choice.
slug: architecture/no-mutation-principle
---
## Core Philosophy
Papra follows a fundamental principle: **documents are never mutated after upload**. When you input a document, you can always retrieve it exactly as it was uploaded.
## The Design Choice
An archiving platform should guarantee users they can retrieve their documents in their original form. This means:
- No conversion to different formats
- No metadata injection into the file itself
- No overlay of OCR-ed content on scanned PDFs
- No processing that modifies the original file
The simple mental model is: **"If I input X, I'll retrieve X"**
## Why This Matters
### Trust and Reliability
When archiving important documents, users need absolute confidence that their files remain untouched. Whether it's a legal document, a medical record, or a personal photo, the original should be sacrosanct.
### Simplicity
This approach eliminates the mental overhead of wondering "what happened to my file?" Users don't need to understand concepts like:
- Original vs. processed versions
- Format conversions
- OCR overlays
- Metadata injection
### Flexibility for the Future
While Papra currently doesn't mutate documents, the architecture leaves room for future enhancements. If needed, a "processed" version concept could be added alongside originals, giving users the choice without forcing a particular model.

View File

@@ -0,0 +1,62 @@
---
title: Organization Deletion & Purge
description: How Papra handles organization deletion with a grace period and eventual purge.
slug: architecture/organization-deletion-purge
---
## Overview
Papra implements a two-phase deletion process for organizations: soft deletion followed by hard deletion (purge). This provides a grace period for recovery while ensuring eventual cleanup of resources.
## Deletion Process
### Who Can Delete
Only the **organization owner** can delete an organization. Admins and members do not have this permission.
### What Happens During Deletion
When an organization is deleted:
1. **Members are removed** - All organization members are stripped from the organization, leaving them dangling
2. **Invitations are removed** - All pending invitations are deleted
3. **Metadata is recorded**:
- `deletedAt`: Timestamp when the deletion occurred
- `deletedBy`: ID of the user (owner) who deleted the organization
- `scheduledPurgeAt`: Future date when hard deletion will occur (default: 30 days)
The organization itself remains in the database in a soft-deleted state, allowing for potential restoration.
## Purge Process
### When Purge Occurs
Hard deletion (purge) happens when `scheduledPurgeAt` is reached. By default, this is **30 days** after the deletion date.
### What Gets Purged
When an organization is purged:
- **All documents** are deleted from storage
- **All database records** related to the organization are removed (cascade handles related records, like Tags, Intake Emails, etc.)
- The organization itself is permanently deleted
The process handles documents in batches using an iterator to avoid memory issues with large organizations.
### Background Task
Purging is handled by a periodic background task that:
1. Queries for organizations with `scheduledPurgeAt` in the past
2. For each expired organization:
- Deletes all document files from storage
- Hard deletes the organization (cascade handles related records)
3. Logs the process for monitoring and debugging
The task continues even if individual file deletions fail, logging errors without blocking the entire purge operation.
## Recovery
Organizations can be restored before the `scheduledPurgeAt` date is reached, but only by the user who deleted them (the previous owner). After this date, recovery is no longer possible, even if the purge has not yet occurred.
> Note: After recovery, the organization owner must re-invite members as they were removed during deletion.

View File

@@ -1,6 +1,6 @@
---
title: Papra documentation
description: Papra documentation.
description: Documentation for Papra, the minimalistic document archiving platform.
hero:
title: Papra Docs
tagline: Documentation for Papra, the minimalistic document archiving platform.
@@ -53,9 +53,9 @@ In today's digital world, managing countless important documents efficiently and
- **Content extraction**: Automatically extract text from images or scanned documents for search.
- **Tagging Rules**: Automatically tag documents based on custom rules.
- **Folder ingestion**: Automatically import documents from a folder.
- *In progress:* **i18n**: Support for multiple languages.
- *Coming soon:* **SDK and API**: Build your own applications on top of Papra.
- *Coming soon:* **CLI**: Manage your documents from the command line.
- **API, SDK and webhooks**: Build your own applications on top of Papra.
- **CLI**: Manage your documents from the command line.
- **i18n**: Support for multiple languages.
- *Coming soon:* **Document sharing**: Share documents with others.
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.

View File

@@ -1,10 +1,11 @@
import type { StarlightUserConfig } from '@astrojs/starlight/types';
export const sidebar: StarlightUserConfig['sidebar'] = [
export const sidebar = [
{
label: 'Getting Started',
items: [
{ label: 'Introduction', slug: '' },
{ label: 'Changelog', link: '/changelog' },
],
},
{
@@ -12,6 +13,7 @@ export const sidebar: StarlightUserConfig['sidebar'] = [
items: [
{ label: 'Using Docker', slug: 'self-hosting/using-docker' },
{ label: 'Using Docker Compose', slug: 'self-hosting/using-docker-compose' },
{ label: 'Docker Compose Generator', link: '/docker-compose-generator', badge: { text: 'new', variant: 'note' } },
{ label: 'Configuration', slug: 'self-hosting/configuration' },
],
},
@@ -30,11 +32,40 @@ export const sidebar: StarlightUserConfig['sidebar'] = [
label: 'Setup Ingestion Folder',
slug: 'guides/setup-ingestion-folder',
},
{
label: 'Setup Custom OAuth2 Providers',
slug: 'guides/setup-custom-oauth2-providers',
},
{
label: 'Document Encryption',
slug: 'guides/document-encryption',
},
],
},
{
label: 'Architecture',
items: [
{
label: 'No-Mutation Principle',
slug: 'architecture/no-mutation-principle',
},
{
label: 'Document Deduplication',
slug: 'architecture/document-deduplication',
},
{
label: 'Organization Deletion',
slug: 'architecture/organization-deletion-purge',
},
],
},
{
label: 'Resources',
items: [
{
label: 'Troubleshooting',
slug: 'resources/troubleshooting',
},
{
label: 'CLI Documentation',
slug: 'resources/cli',
@@ -46,6 +77,10 @@ export const sidebar: StarlightUserConfig['sidebar'] = [
target: '_blank',
},
},
{
label: 'API Endpoints',
slug: 'resources/api-endpoints',
},
],
},
];
] satisfies StarlightUserConfig['sidebar'];

View File

@@ -0,0 +1,487 @@
---
import { codeToHtml } from 'shiki';
const images = {
GitHub: 'ghcr.io/papra-hq/papra',
DockerHub: 'corentinth/papra',
};
const defaultDockerCompose = `
services:
papra:
image: ghcr.io/papra-hq/papra:latest
container_name: papra
restart: unless-stopped
ports:
- 1221:1221
environment:
- AUTH_SECRET=change-me
- APP_BASE_URL=http://localhost:1221
volumes:
- ./app-data:/app/app-data
user: 1000:1000
`.trim();
const dcHtml = await codeToHtml(defaultDockerCompose, { theme: 'vitesse-black', lang: 'yaml' });
const defaultCommand = `mkdir -p ./app-data/{db,documents} && docker compose up -d`;
---
<h2 class="mt-8 mb-2">General settings</h2>
<div class="flex items-center gap-2 mt-1">
<label for="port" class="min-w-32">External port</label>
<input id="port" class="input-field" value="1221" type="number" min="1024" max="65535" placeholder="eg: 1221" />
</div>
<div class="flex items-center gap-2 mt-1">
<label for="app-base-url" class="min-w-32">App base URL</label>
<input id="app-base-url" class="input-field" type="text" placeholder="eg: https://papra.example.com" value="http://localhost:1221" />
</div>
<div class="flex items-center gap-2 mt-1">
<label for="source" class="min-w-32">Image source</label>
<select class="input-field mt-0" id="source">
{Object.entries(images).map(([registry, imageName]) => <option class="bg-background" value={imageName}>{`${registry} - ${imageName}`}</option>)}
</select>
</div>
<div class="flex items-center gap-2 mt-1">
<label for="service-name" class="min-w-32">Service Name</label>
<input id="service-name" class="input-field" value="papra" type="text" placeholder="eg: papra" />
</div>
<div class="flex items-center gap-2 mt-1">
<label
for="auth-secret"
class="min-w-32"
>
Auth secret
</label>
<div class="flex items-center gap-2 mt-0 w-full">
<input class="input-field font-mono" id="auth-secret" type="text" placeholder="eg: 1234567890" />
<button class="btn bg-muted" id="refresh-secret"> Refresh </button>
</div>
</div>
<div class="flex items-center gap-2 mt-1">
<label for="volume-path" class="min-w-32">Volume path</label>
<input id="volume-path" class="input-field" value="./app-data" type="text" placeholder="eg: ./app-data" />
</div>
<div class="flex items-center gap-2 mt-1">
<label for="privileged-mode" class="min-w-32">Privileged mode</label>
<div class="flex items-center gap-2 mt-0 w-full">
<select class="input-field mt-0" id="privileged-mode">
<option value="false" class="bg-background">Rootless</option>
<option value="true" class="bg-background">Root</option>
</select>
</div>
</div>
<h2 class="mt-8 mb-2">Ingestion folder</h2>
<div class="flex items-center gap-2 mt-1">
<label for="ingestion-enabled" class="min-w-32">Enable ingestion</label>
<div class="flex items-center gap-2 mt-0 w-full">
<select class="input-field mt-0" id="ingestion-enabled">
<option value="false" class="bg-background">Disabled</option>
<option value="true" class="bg-background">Enabled</option>
</select>
</div>
</div>
<div class="flex items-center gap-2 mt-1" id="ingestion-path-container" style="display: none;">
<label for="ingestion-path" class="min-w-32">Ingestion path</label>
<input id="ingestion-path" class="input-field" value="./ingestion" type="text" placeholder="eg: ./ingestion" />
</div>
<h2 class="mt-8 mb-2">Intake emails</h2>
<div class="flex items-center gap-2 mt-1">
<label for="intake-email-enabled" class="min-w-32">Enabled</label>
<div class="flex items-center gap-2 mt-0 w-full">
<select class="input-field mt-0" id="intake-email-enabled">
<option value="false" class="bg-background">Disabled</option>
<option value="true" class="bg-background">Enabled</option>
</select>
</div>
</div>
<div class="flex items-center gap-2 mt-1" id="intake-email-driver-container" style="display: none;">
<label for="intake-email-driver" class="min-w-32">Driver</label>
<div class="flex items-center gap-2 mt-0 w-full">
<select class="input-field mt-0" id="intake-email-driver">
<option value="owlrelay" class="bg-background">OwlRelay</option>
<option value="random-username" class="bg-background">Cloudflare Email Worker</option>
</select>
</div>
</div>
<div id="intake-email-owlrelay-config" style="display: none;" class="mt-1">
<div class="flex items-center gap-2 mt-1">
<label for="intake-email-owlrelay-api-key" class="min-w-32">API Key</label>
<input id="intake-email-owlrelay-api-key" class="input-field" type="text" placeholder="owrl_*****" />
</div>
<div class="flex items-center gap-2 mt-1">
<label for="intake-email-owlrelay-webhook-url" class="min-w-32">Webhook URL</label>
<input id="intake-email-owlrelay-webhook-url" class="input-field" type="text" placeholder="https://your-instance.com/api/intake-emails/ingest" value="https://localhost:1221/api/intake-emails/ingest" />
</div>
</div>
<div id="intake-email-cf-worker-config" style="display: none;" class="mt-1">
<div class="flex items-center gap-2 mt-1">
<label for="intake-email-cf-email-domain" class="min-w-32">Email domain</label>
<input id="intake-email-cf-email-domain" class="input-field" type="text" placeholder="papra.email" />
</div>
</div>
<div class="flex items-center gap-2 mt-1" id="intake-email-webhook-secret-container" style="display: none;">
<label for="intake-email-webhook-secret" class="min-w-32">Webhook secret</label>
<div class="flex items-center gap-2 mt-0 w-full">
<input class="input-field font-mono" id="intake-email-webhook-secret" type="text" placeholder="a-random-key" />
<button class="btn bg-muted" id="refresh-webhook-secret">Refresh</button>
</div>
</div>
<div id="docker-compose-output" class="mt-12" set:html={dcHtml} />
<pre id="command-output" class="bg-card p-4 rounded-md text-muted-foreground text-sm font-mono overflow-x-auto">{defaultCommand}</pre>
<div class="flex items-center gap-2 mt-4">
<button class="btn bg-muted mt-0" id="download-button">Download docker-compose.yml</button>
<button class="btn bg-muted mt-0" id="copy-button">Copy docker compose to clipboard</button>
<button class="btn bg-muted mt-0" id="copy-command-button">Copy command</button>
</div>
<script>
import { codeToHtml } from 'shiki';
import { stringify } from 'yaml';
const portInput = document.getElementById('port') as HTMLInputElement;
const sourceSelect = document.getElementById('source') as HTMLSelectElement;
const serviceNameInput = document.getElementById('service-name') as HTMLInputElement;
const authSecretInput = document.getElementById('auth-secret') as HTMLInputElement;
const appBaseUrlInput = document.getElementById('app-base-url') as HTMLInputElement;
const refreshSecretButton = document.getElementById('refresh-secret');
const copyButton = document.getElementById('copy-button');
const dockerComposeOutput = document.getElementById('docker-compose-output');
const downloadButton = document.getElementById('download-button');
const volumePathInput = document.getElementById('volume-path') as HTMLInputElement;
const privilegedModeSelect = document.getElementById('privileged-mode') as HTMLSelectElement;
const ingestionEnabledSelect = document.getElementById('ingestion-enabled') as HTMLSelectElement;
const ingestionPathInput = document.getElementById('ingestion-path') as HTMLInputElement;
const ingestionPathContainer = document.getElementById('ingestion-path-container') as HTMLDivElement;
const intakeEmailEnabledSelect = document.getElementById('intake-email-enabled') as HTMLSelectElement;
const intakeDriverSelect = document.getElementById('intake-email-driver') as HTMLSelectElement;
const owlrelayConfig = document.getElementById('intake-email-owlrelay-config') as HTMLDivElement;
const cfWorkerConfig = document.getElementById('intake-email-cf-worker-config') as HTMLDivElement;
const owlrelayApiKeyInput = document.getElementById('intake-email-owlrelay-api-key') as HTMLInputElement;
const owlrelayWebhookUrlInput = document.getElementById('intake-email-owlrelay-webhook-url') as HTMLInputElement;
const cfEmailDomainInput = document.getElementById('intake-email-cf-email-domain') as HTMLInputElement;
const webhookSecretInput = document.getElementById('intake-email-webhook-secret') as HTMLInputElement;
const refreshWebhookSecretButton = document.getElementById('refresh-webhook-secret');
const commandOutput = document.getElementById('command-output');
const copyCommandButton = document.getElementById('copy-command-button');
// Track whether the app base URL has been customized by the user
let isAppBaseUrlCustomized = false;
// Track whether the webhook URL has been customized by the user
let isWebhookUrlCustomized = false;
function getRandomString() {
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
return Array.from({ length: 48 }, () => alphabet[Math.floor(Math.random() * alphabet.length)]).join('');
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
function isDefaultAppBaseUrl(url: string, port: string): boolean {
return url === `http://localhost:${port}`;
}
function generateDefaultWebhookUrl(baseUrl: string): string {
// Remove trailing slash if present
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
return `${cleanBaseUrl}/api/intake-emails/ingest`;
}
function isDefaultWebhookUrl(webhookUrl: string, baseUrl: string): boolean {
return webhookUrl === generateDefaultWebhookUrl(baseUrl);
}
function refreshIsWebhookUrlCustomized() {
const currentBaseUrl = appBaseUrlInput.value.trim();
const currentWebhookUrl = owlrelayWebhookUrlInput.value.trim();
if (isDefaultWebhookUrl(currentWebhookUrl, currentBaseUrl)) {
isWebhookUrlCustomized = false;
} else {
isWebhookUrlCustomized = true;
}
}
function refreshIsAppBaseUrlCustomized() {
const currentPort = portInput.value;
const currentUrl = appBaseUrlInput.value.trim();
if (isDefaultAppBaseUrl(currentUrl, currentPort)) {
isAppBaseUrlCustomized = false;
} else {
isAppBaseUrlCustomized = true;
}
}
function updateWebhookUrlFromBaseUrl() {
if (!isWebhookUrlCustomized) {
const baseUrl = appBaseUrlInput.value.trim();
if (baseUrl) {
owlrelayWebhookUrlInput.value = generateDefaultWebhookUrl(baseUrl);
}
}
}
function updateAppBaseUrlFromPort() {
if (!isAppBaseUrlCustomized) {
const port = portInput.value;
appBaseUrlInput.value = `http://localhost:${port}`;
// Also update webhook URL when app base URL changes
updateWebhookUrlFromBaseUrl();
}
}
function handlePortChange() {
updateAppBaseUrlFromPort();
updateDockerCompose();
}
function handleAppBaseUrlChange() {
refreshIsAppBaseUrlCustomized();
updateWebhookUrlFromBaseUrl();
updateDockerCompose();
}
function handleWebhookUrlChange() {
refreshIsWebhookUrlCustomized();
updateDockerCompose();
}
function getDockerComposeYml() {
const serviceName = serviceNameInput.value;
const isRootless = privilegedModeSelect.value === 'false';
const image = sourceSelect.value;
const port = portInput.value;
const authSecret = authSecretInput.value;
const volumePath = volumePathInput.value;
const isIngestionEnabled = ingestionEnabledSelect.value === 'true';
const ingestionPath = ingestionPathInput.value;
const intakeEmailEnabled = intakeEmailEnabledSelect.value === 'true';
const intakeDriver = intakeDriverSelect.value;
const webhookSecret = webhookSecretInput.value;
const appBaseUrl = appBaseUrlInput.value.trim() || `http://localhost:${port}`;
const version = isRootless ? 'latest' : 'latest-root';
const fullImage = `${image}:${version}`;
const environment = [
`AUTH_SECRET=${authSecret}`,
`APP_BASE_URL=${appBaseUrl}`,
isIngestionEnabled && 'INGESTION_FOLDER_IS_ENABLED=true',
intakeEmailEnabled && 'INTAKE_EMAILS_IS_ENABLED=true',
intakeEmailEnabled && `INTAKE_EMAILS_DRIVER=${intakeDriver}`,
intakeEmailEnabled && `INTAKE_EMAILS_WEBHOOK_SECRET=${webhookSecret}`,
intakeEmailEnabled && intakeDriver === 'owlrelay' && owlrelayApiKeyInput.value && `OWLRELAY_API_KEY=${owlrelayApiKeyInput.value}`,
intakeEmailEnabled && intakeDriver === 'owlrelay' && owlrelayWebhookUrlInput.value && `OWLRELAY_WEBHOOK_URL=${owlrelayWebhookUrlInput.value}`,
intakeEmailEnabled && intakeDriver === 'random-username' && cfEmailDomainInput.value && `INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN=${cfEmailDomainInput.value}`,
].flat().filter(Boolean);
const volumes = [
`${volumePath}:/app/app-data`,
isIngestionEnabled && `${ingestionPath}:/app/ingestion`,
].filter(Boolean);
const dc = {
services: {
[serviceName]: {
image: fullImage,
container_name: serviceName,
restart: 'unless-stopped',
ports: [`${port}:1221`],
environment,
volumes,
...(isRootless && {
user: '1000:1000',
}),
},
},
};
return stringify(dc);
}
function getStartCommand() {
const volumePath = volumePathInput.value;
const volumePathNormalized = volumePath.replace(/\/$/, '');
const volumeWithSubdirs = `${volumePathNormalized}/{db,documents}`;
const mkdirCommand = `mkdir -p ${volumeWithSubdirs}`;
const dockerCommand = 'docker compose up -d';
return `${mkdirCommand} && ${dockerCommand}`;
}
async function updateDockerCompose() {
const dockerCompose = getDockerComposeYml();
const command = getStartCommand();
const html = await codeToHtml(dockerCompose, { theme: 'vitesse-black', lang: 'yaml' });
if (dockerComposeOutput) {
dockerComposeOutput.innerHTML = html;
}
if (commandOutput) {
commandOutput.textContent = command;
}
}
function handleCopy() {
const dockerCompose = getDockerComposeYml();
copyToClipboard(dockerCompose);
if (copyButton) {
copyButton.textContent = 'Copied!';
}
setTimeout(() => {
if (copyButton) {
copyButton.textContent = 'Copy to clipboard';
}
}, 1000);
}
function handleRefreshSecret() {
authSecretInput.value = getRandomString();
updateDockerCompose();
}
function handleDownload() {
const dockerCompose = getDockerComposeYml();
const blob = new Blob([dockerCompose], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'docker-compose.yml';
a.click();
}
function handleIngestionEnabledChange() {
const isEnabled = ingestionEnabledSelect.value === 'true';
ingestionPathContainer.style.display = isEnabled ? 'flex' : 'none';
updateDockerCompose();
}
function handleIntakeEmailEnabledChange() {
const isEnabled = intakeEmailEnabledSelect.value === 'true';
const driverContainer = document.getElementById('intake-email-driver-container');
const webhookSecretContainer = document.getElementById('intake-email-webhook-secret-container');
if (driverContainer) {
driverContainer.style.display = isEnabled ? 'flex' : 'none';
}
if (webhookSecretContainer) {
webhookSecretContainer.style.display = isEnabled ? 'flex' : 'none';
}
if (!isEnabled) {
// Reset driver-specific configs when disabled
if (owlrelayConfig) {
owlrelayConfig.style.display = 'none';
}
if (cfWorkerConfig) {
cfWorkerConfig.style.display = 'none';
}
} else {
// Show the appropriate driver config
handleIntakeDriverChange();
}
updateDockerCompose();
}
function handleIntakeDriverChange() {
const driver = intakeDriverSelect.value;
const isEnabled = intakeEmailEnabledSelect.value === 'true';
if (!isEnabled) {
return;
}
if (owlrelayConfig) {
owlrelayConfig.style.display = driver === 'owlrelay' ? 'block' : 'none';
}
if (cfWorkerConfig) {
cfWorkerConfig.style.display = driver === 'random-username' ? 'block' : 'none';
}
updateDockerCompose();
}
function handleRefreshWebhookSecret() {
webhookSecretInput.value = getRandomString();
updateDockerCompose();
}
function handleCopyCommand() {
const command = getStartCommand();
copyToClipboard(command);
if (copyCommandButton) {
copyCommandButton.textContent = 'Copied!';
}
setTimeout(() => {
if (copyCommandButton) {
copyCommandButton.textContent = 'Copy command';
}
}, 1000);
}
// Add event listeners
portInput.addEventListener('input', handlePortChange);
sourceSelect.addEventListener('change', updateDockerCompose);
serviceNameInput.addEventListener('input', updateDockerCompose);
authSecretInput.addEventListener('input', updateDockerCompose);
appBaseUrlInput.addEventListener('input', handleAppBaseUrlChange);
refreshSecretButton?.addEventListener('click', handleRefreshSecret);
copyButton?.addEventListener('click', handleCopy);
downloadButton?.addEventListener('click', handleDownload);
volumePathInput.addEventListener('input', updateDockerCompose);
privilegedModeSelect.addEventListener('change', updateDockerCompose);
ingestionEnabledSelect.addEventListener('change', handleIngestionEnabledChange);
ingestionPathInput.addEventListener('input', updateDockerCompose);
intakeEmailEnabledSelect.addEventListener('change', handleIntakeEmailEnabledChange);
intakeDriverSelect.addEventListener('change', handleIntakeDriverChange);
owlrelayApiKeyInput.addEventListener('input', updateDockerCompose);
owlrelayWebhookUrlInput.addEventListener('input', handleWebhookUrlChange);
cfEmailDomainInput.addEventListener('input', updateDockerCompose);
webhookSecretInput.addEventListener('input', updateDockerCompose);
refreshWebhookSecretButton?.addEventListener('click', handleRefreshWebhookSecret);
copyCommandButton?.addEventListener('click', handleCopyCommand);
authSecretInput.value = getRandomString();
// Initial render
updateDockerCompose();
// Initial setup
handleIngestionEnabledChange();
handleIntakeEmailEnabledChange();
webhookSecretInput.value = getRandomString();
</script>

16
apps/docs/src/markdown.ts Normal file
View File

@@ -0,0 +1,16 @@
import { marked } from 'marked';
const renderer = new marked.Renderer();
renderer.heading = function ({ text, depth }) {
const slug = text.toLowerCase().replace(/\W+/g, '-');
return `
<div class="sl-heading-wrapper level-h${depth}">
<h${depth} id="${slug}">${text}</h${depth}>
<a class="sl-anchor-link" href="#${slug}"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only">Section titled “Configuration files”</span></a>
</div>
`.trim().replace(/\n/g, '');
};
export function renderMarkdown(markdown: string) {
return marked.parse(markdown, { renderer });
}

View File

@@ -0,0 +1,55 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import rawChangelog from '../../../../packages/docker/CHANGELOG.md?raw';
import { parseChangelog } from '../changelog-parser';
import { renderMarkdown } from '../markdown';
const changelog = parseChangelog(rawChangelog);
---
<StarlightPage
frontmatter={{
title: 'Papra changelog',
description: 'View the changelogs of the docker images released by Papra.',
tableOfContents: false,
}}
>
<p>
Here are the changelogs of the docker images released by Papra.<br />
For version after v0.9.6, Papra uses Calver as a versioning system with the format YY.MM.N where N is the number of releases in the month starting at 0 (e.g. 25.06.0 is the first release of June 2025).
</p>
{
changelog.map(({ entries, version }) => (
<section>
<h2 id={version} class="pb-1 mt-14">v{version}</h2>
<ul>
{entries.map(entry => (
<li>
<div class="flex flex-col">
<div class="text-foreground lh-normal changelog-entry" set:html={renderMarkdown(entry.content)} />
<div class="text-xs mt-1 flex gap-1 flex-wrap">
<a href={entry.pr.url} class="text-muted-foreground hover:bg-muted transition border border-muted border-solid rounded-md no-underline px-1 py-0.5">PR #{entry.pr.number}</a>
<a href={entry.commit.url} class="text-muted-foreground hover:bg-muted transition border border-muted border-solid rounded-md no-underline px-1 py-0.5">{entry.commit.hash.slice(0, 7)}</a>
<a href={entry.contributor.url} class="text-muted-foreground hover:bg-muted transition border border-muted border-solid rounded-md no-underline px-1 py-0.5">
By @{entry.contributor.username}
</a>
</div>
</div>
</li>
))}
</ul>
</section>
))
}
</StarlightPage>
<style is:global>
.changelog-entry pre {
border-radius: 6px;
color: hsl(var(--muted-foreground) / var(--un-text-opacity));
}
</style>

View File

@@ -0,0 +1,16 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import DockerComposeGeneratorComp from '../docker-compose-generator/dc-generator.astro';
---
<StarlightPage
frontmatter={{
title: 'Papra docker-compose.yml generator',
description: 'Generate a custom docker-compose.yml file for Papra, tailored to your needs.',
tableOfContents: false,
}}
>
<p>This tool will help you generate a custom docker-compose.yml file for Papra, tailored to your needs. You can personalize the service name, the port, the auth secret, and the source image.</p>
<p>For more configuration options, you can use the <a href="/self-hosting/configuration">configuration reference</a>.</p>
<DockerComposeGeneratorComp />
</StarlightPage>

View File

@@ -0,0 +1,28 @@
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
import { sidebar } from '../content/navigation';
export const GET: APIRoute = async ({ site }) => {
const docs = await getCollection('docs');
const sections = sidebar.map((section) => {
return {
label: section.label,
items: section
.items
.filter(item => item.slug !== undefined || (item.link && !item.link.startsWith('http')))
.map((item) => {
const slug = item.slug ?? item.link?.replace(/^\//, '');
return {
label: item.label,
slug,
url: new URL(slug, site).toString(),
description: docs.find(doc => (doc.id === slug || (slug === '' && doc.id === 'index')))?.data.description,
};
}),
};
});
return new Response(JSON.stringify(sections));
};

View File

@@ -1,5 +1,10 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
]
}

99
apps/docs/uno.config.ts Normal file
View File

@@ -0,0 +1,99 @@
import {
defineConfig,
presetTypography,
presetUno,
transformerDirectives,
transformerVariantGroup,
} from 'unocss';
import presetAnimations from 'unocss-preset-animations';
export default defineConfig({
presets: [
presetUno({
dark: {
dark: '[data-theme="dark"]',
light: '[data-theme="light"]',
},
prefix: '',
}),
presetAnimations(),
presetTypography(),
],
transformers: [transformerVariantGroup(), transformerDirectives()],
theme: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
animation: {
keyframes: {
'accordion-down':
'{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }',
'accordion-up':
'{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }',
'collapsible-down':
'{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }',
'collapsible-up':
'{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }',
'caret-blink': '{ 0%,70%,100% { opacity: 1 } 20%,50% { opacity: 0 } }',
},
timingFns: {
'accordion-down': 'ease-out',
'accordion-up': 'ease-out',
'collapsible-down': 'ease-out',
'collapsible-up': 'ease-out',
'caret-blink': 'ease-out',
},
durations: {
'accordion-down': '0.2s',
'accordion-up': '0.2s',
'collapsible-down': '0.2s',
'collapsible-up': '0.2s',
'caret-blink': '1.25s',
},
counts: {
'caret-blink': 'infinite',
},
},
},
shortcuts: {
'input-field': 'flex h-9 w-full bg-none outline-none rounded-lg border border-border border-solid bg-inherit px-3 py-1 text-sm shadow-none placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow',
'btn': 'text-sm font-medium hover:opacity-80 rounded-lg transition-all px-4 py-2 bg-none outline-none border-none cursor-pointer',
},
});

1
apps/papra-client/.nvmrc Normal file
View File

@@ -0,0 +1 @@
22

View File

@@ -1,23 +0,0 @@
# @papra/app-client
## 0.4.0
### Minor Changes
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added webhook management
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added API keys support
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added document searchable content edit
### Patch Changes
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added tag creation button in document page
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Improved tag selector input wrapping
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Properly handle file names without extensions
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Wrap text in document preview
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Excluded deleted documents from doc count

View File

@@ -5,6 +5,10 @@ export default antfu({
semi: true,
},
ignores: [
'public/manifest.json',
],
rules: {
// To allow export on top of files
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
@@ -18,4 +22,10 @@ export default antfu({
caughtErrorsIgnorePattern: '^_',
}],
},
}, {
files: ['src/locales/*.dictionary.ts'],
rules: {
// Sometimes for formatting amounts of dollar, we need "${{value}}" as value is interpolated later, it's not a template string here
'no-template-curly-in-string': 'off',
},
});

View File

@@ -27,10 +27,23 @@
<meta property="twitter:image" content="https://papra.app/og-image.png">
<!-- Favicon and Icons -->
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<!-- Structured Data (JSON-LD for rich snippets) -->
<script type="application/ld+json">
@@ -52,8 +65,19 @@
</script>
<style>.sr-only {position: absolute;width: 1px;height: 1px;padding: 0;margin: -1px;overflow: hidden;clip: rect(0, 0, 0, 0);white-space: nowrap;border-width: 0;}</style>
<!-- Prevent flash of wrong theme on load -->
<script>
(function () {
const stored = localStorage?.getItem('papra_color_mode') ?? 'dark';
if (stored === 'dark') {
document.documentElement.setAttribute('data-kb-theme', 'dark');
}
})();
</script>
</head>
<body>
<body class="bg-background text-foreground">
<h1 class="sr-only">Papra - Document archiving and sharing platform</h1>
<p class="sr-only">Papra, the document archiving and sharing platform.</p>

View File

@@ -1,9 +1,9 @@
{
"name": "@papra/app-client",
"type": "module",
"version": "0.4.0",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@10.9.0",
"packageManager": "pnpm@10.12.3",
"description": "Papra frontend client",
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
"license": "AGPL-3.0-or-later",
@@ -25,52 +25,49 @@
"test:watch": "vitest watch",
"test:e2e": "playwright test",
"typecheck": "tsc --noEmit",
"script:get-missing-i18n-keys": "tsx src/scripts/get-missing-i18n-keys.script.ts",
"script:generate-i18n-types": "tsx src/scripts/generate-i18n-types.script.ts"
"script:sync-i18n-key-order": "tsx src/scripts/sync-i18n-key-order.script.ts"
},
"dependencies": {
"@corentinth/chisels": "^1.0.2",
"@kobalte/core": "^0.13.7",
"@branchlet/core": "^1.0.0",
"@corentinth/chisels": "^1.3.1",
"@kobalte/core": "^0.13.10",
"@kobalte/utils": "^0.9.1",
"@modular-forms/solid": "^0.25.0",
"@pdfslick/solid": "^2.0.0",
"@solid-primitives/storage": "^4.2.1",
"@solidjs/router": "^0.14.3",
"@tanstack/solid-query": "^5.61.5",
"@tanstack/solid-table": "^8.20.5",
"@unocss/reset": "^0.64.0",
"@modular-forms/solid": "^0.25.1",
"@pdfslick/solid": "^2.3.0",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.14.10",
"@tanstack/solid-query": "^5.90.3",
"@tanstack/solid-table": "^8.21.3",
"@unocss/reset": "^0.64.1",
"better-auth": "catalog:",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk-solid": "^1.1.0",
"cmdk-solid": "^1.1.2",
"date-fns": "^4.1.0",
"lodash-es": "^4.17.21",
"ofetch": "^1.4.1",
"posthog-js": "^1.231.0",
"posthog-js-lite": "^4.1.5",
"radix3": "^1.1.2",
"solid-js": "^1.8.11",
"solid-js": "^1.9.9",
"solid-sonner": "^0.2.8",
"tailwind-merge": "^2.6.0",
"ts-pattern": "^5.5.0",
"unocss-preset-animations": "^1.1.0",
"unstorage": "^1.14.4",
"ts-pattern": "^5.7.1",
"unocss-preset-animations": "^1.2.1",
"unstorage": "^1.16.0",
"valibot": "1.0.0-beta.10"
},
"devDependencies": {
"@antfu/eslint-config": "catalog:",
"@iconify-json/tabler": "^1.1.120",
"@playwright/test": "^1.46.1",
"@types/lodash-es": "^4.17.12",
"@iconify-json/tabler": "^1.2.19",
"@playwright/test": "^1.53.1",
"@types/node": "catalog:",
"eslint": "catalog:",
"jsdom": "^25.0.0",
"tinyglobby": "^0.2.13",
"tsx": "^4.19.1",
"jsdom": "^25.0.1",
"tinyglobby": "^0.2.14",
"tsx": "^4.20.3",
"typescript": "catalog:",
"unocss": "0.65.0-beta.2",
"vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2",
"vitest": "catalog:",
"yaml": "^2.7.0"
"unocss": "66.5.3",
"vite": "^7.1.9",
"vite-plugin-solid": "^2.11.9",
"vitest": "catalog:"
}
}

View File

@@ -0,0 +1,3 @@
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,41 @@
{
"name": "Papra",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -21,10 +21,10 @@
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--destructive-foreground: 0 0% 0%;
--warning: 31 98% 50%;
--warning-foreground: 0 0% 98%;
--warning-foreground: 0 0% 0%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
@@ -59,7 +59,7 @@
--destructive-foreground: 0 0% 98%;
--warning: 31 98% 50%;
--warning-foreground: 0 0% 98%;
--warning-foreground: 0 0% 0%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;

View File

@@ -1,7 +1,7 @@
/* @refresh reload */
import type { ConfigColorMode } from '@kobalte/core/color-mode';
import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from '@kobalte/core/color-mode';
import { ColorModeProvider, createLocalStorageManager } from '@kobalte/core/color-mode';
import { Router } from '@solidjs/router';
import { QueryClientProvider } from '@tanstack/solid-query';
@@ -9,6 +9,7 @@ import { render, Suspense } from 'solid-js/web';
import { CommandPaletteProvider } from './modules/command-palette/command-palette.provider';
import { ConfigProvider } from './modules/config/config.provider';
import { DemoIndicator } from './modules/demo/demo.provider';
import { RenameDocumentDialogProvider } from './modules/documents/components/rename-document-button.component';
import { I18nProvider } from './modules/i18n/i18n.provider';
import { ConfirmModalProvider } from './modules/shared/confirm';
import { queryClient } from './modules/shared/query/query-client';
@@ -27,26 +28,26 @@ render(
const localStorageManager = createLocalStorageManager(colorModeStorageKey);
return (
<Router
children={routes}
root={props => (
<QueryClientProvider client={queryClient}>
<PageViewTracker />
<IdentifyUser />
<QueryClientProvider client={queryClient}>
<Router
children={routes}
root={props => (
<Suspense>
<PageViewTracker />
<IdentifyUser />
<I18nProvider>
<ConfirmModalProvider>
<ColorModeScript storageType={localStorageManager.type} storageKey={colorModeStorageKey} initialColorMode={initialColorMode} />
<ColorModeProvider
initialColorMode={initialColorMode}
storageManager={localStorageManager}
>
<CommandPaletteProvider>
<ConfigProvider>
<div class="min-h-screen font-sans text-sm font-400">
{props.children}
</div>
<RenameDocumentDialogProvider>
<div class="min-h-screen font-sans text-sm font-400">
{props.children}
</div>
</RenameDocumentDialogProvider>
<DemoIndicator />
</ConfigProvider>
@@ -57,9 +58,9 @@ render(
</ConfirmModalProvider>
</I18nProvider>
</Suspense>
</QueryClientProvider>
)}
/>
)}
/>
</QueryClientProvider>
);
},
document.getElementById('root')!,

View File

@@ -0,0 +1,665 @@
import type { TranslationsDictionary } from '@/modules/i18n/locales.types';
export const translations: Partial<TranslationsDictionary> = {
// Authentication
'auth.request-password-reset.title': 'Passwort zurücksetzen',
'auth.request-password-reset.description': 'Geben Sie Ihre E-Mail-Adresse ein, um Ihr Passwort zurückzusetzen.',
'auth.request-password-reset.requested': 'Wenn ein Konto mit dieser E-Mail-Adresse existiert, haben wir Ihnen eine E-Mail zum Zurücksetzen Ihres Passworts gesendet.',
'auth.request-password-reset.back-to-login': 'Zurück zum Login',
'auth.request-password-reset.form.email.label': 'E-Mail',
'auth.request-password-reset.form.email.placeholder': 'Beispiel: ada@papra.app',
'auth.request-password-reset.form.email.required': 'Bitte geben Sie Ihre E-Mail-Adresse ein',
'auth.request-password-reset.form.email.invalid': 'Diese E-Mail-Adresse ist ungültig',
'auth.request-password-reset.form.submit': 'Passwort zurücksetzen anfordern',
'auth.reset-password.title': 'Passwort zurücksetzen',
'auth.reset-password.description': 'Geben Sie Ihr neues Passwort ein, um Ihr Passwort zurückzusetzen.',
'auth.reset-password.reset': 'Ihr Passwort wurde zurückgesetzt.',
'auth.reset-password.back-to-login': 'Zurück zum Login',
'auth.reset-password.form.new-password.label': 'Neues Passwort',
'auth.reset-password.form.new-password.placeholder': 'Beispiel: **********',
'auth.reset-password.form.new-password.required': 'Bitte geben Sie Ihr neues Passwort ein',
'auth.reset-password.form.new-password.min-length': 'Das Passwort muss mindestens {{ minLength }} Zeichen lang sein',
'auth.reset-password.form.new-password.max-length': 'Das Passwort muss weniger als {{ maxLength }} Zeichen lang sein',
'auth.reset-password.form.submit': 'Passwort zurücksetzen',
'auth.email-provider.open': '{{ provider }} öffnen',
'auth.login.title': 'Bei Papra anmelden',
'auth.login.description': 'Geben Sie Ihre E-Mail-Adresse ein oder verwenden Sie die soziale Anmeldung, um auf Ihr Papra-Konto zuzugreifen.',
'auth.login.login-with-provider': 'Mit {{ provider }} anmelden',
'auth.login.no-account': 'Sie haben noch kein Konto?',
'auth.login.register': 'Registrieren',
'auth.login.form.email.label': 'E-Mail',
'auth.login.form.email.placeholder': 'Beispiel: ada@papra.app',
'auth.login.form.email.required': 'Bitte geben Sie Ihre E-Mail-Adresse ein',
'auth.login.form.email.invalid': 'Diese E-Mail-Adresse ist ungültig',
'auth.login.form.password.label': 'Passwort',
'auth.login.form.password.placeholder': 'Passwort festlegen',
'auth.login.form.password.required': 'Bitte geben Sie Ihr Passwort ein',
'auth.login.form.remember-me.label': 'Angemeldet bleiben',
'auth.login.form.forgot-password.label': 'Passwort vergessen?',
'auth.login.form.submit': 'Anmelden',
'auth.register.title': 'Bei Papra registrieren',
'auth.register.description': 'Erstellen Sie ein Konto, um Papra zu nutzen.',
'auth.register.register-with-email': 'Mit E-Mail registrieren',
'auth.register.register-with-provider': 'Mit {{ provider }} registrieren',
'auth.register.providers.google': 'Google',
'auth.register.providers.github': 'GitHub',
'auth.register.have-account': 'Sie haben bereits ein Konto?',
'auth.register.login': 'Anmelden',
'auth.register.registration-disabled.title': 'Registrierung ist deaktiviert',
'auth.register.registration-disabled.description': 'Die Erstellung neuer Konten ist auf dieser Papra-Instanz derzeit deaktiviert. Nur Benutzer mit bestehenden Konten können sich anmelden. Wenn Sie dies für einen Fehler halten, wenden Sie sich bitte an den Administrator dieser Instanz.',
'auth.register.form.email.label': 'E-Mail',
'auth.register.form.email.placeholder': 'Beispiel: ada@papra.app',
'auth.register.form.email.required': 'Bitte geben Sie Ihre E-Mail-Adresse ein',
'auth.register.form.email.invalid': 'Diese E-Mail-Adresse ist ungültig',
'auth.register.form.password.label': 'Passwort',
'auth.register.form.password.placeholder': 'Passwort festlegen',
'auth.register.form.password.required': 'Bitte geben Sie Ihr Passwort ein',
'auth.register.form.password.min-length': 'Das Passwort muss mindestens {{ minLength }} Zeichen lang sein',
'auth.register.form.password.max-length': 'Das Passwort muss weniger als {{ maxLength }} Zeichen lang sein',
'auth.register.form.name.label': 'Name',
'auth.register.form.name.placeholder': 'Beispiel: Ada Lovelace',
'auth.register.form.name.required': 'Bitte geben Sie Ihren Namen ein',
'auth.register.form.name.max-length': 'Der Name muss weniger als {{ maxLength }} Zeichen lang sein',
'auth.register.form.submit': 'Registrieren',
'auth.email-validation-required.title': 'E-Mail verifizieren',
'auth.email-validation-required.description': 'Eine Verifizierungs-E-Mail wurde an Ihre E-Mail-Adresse gesendet. Bitte verifizieren Sie Ihre E-Mail-Adresse, indem Sie auf den Link in der E-Mail klicken.',
'auth.legal-links.description': 'Indem Sie fortfahren, bestätigen Sie, dass Sie die {{ terms }} und die {{ privacy }} verstanden haben und ihnen zustimmen.',
'auth.legal-links.terms': 'Nutzungsbedingungen',
'auth.legal-links.privacy': 'Datenschutzrichtlinie',
'auth.no-auth-provider.title': 'Kein Authentifizierungsanbieter',
'auth.no-auth-provider.description': 'Es gibt keine Authentifizierungsanbieter auf dieser Papra-Instanz. Bitte kontaktieren Sie den Administrator dieser Instanz, um sie zu aktivieren.',
// User settings
'user.settings.title': 'Benutzereinstellungen',
'user.settings.description': 'Verwalten Sie hier Ihre Kontoeinstellungen.',
'user.settings.email.title': 'E-Mail-Adresse',
'user.settings.email.description': 'Ihre E-Mail-Adresse kann nicht geändert werden.',
'user.settings.email.label': 'E-Mail-Adresse',
'user.settings.name.title': 'Vollständiger Name',
'user.settings.name.description': 'Ihr vollständiger Name wird anderen Organisationsmitgliedern angezeigt.',
'user.settings.name.label': 'Vollständiger Name',
'user.settings.name.placeholder': 'Z.B. Max Mustermann',
'user.settings.name.update': 'Namen aktualisieren',
'user.settings.name.updated': 'Ihr vollständiger Name wurde aktualisiert',
'user.settings.logout.title': 'Abmelden',
'user.settings.logout.description': 'Melden Sie sich von Ihrem Konto ab. Sie können sich später wieder anmelden.',
'user.settings.logout.button': 'Abmelden',
// Organizations
'organizations.list.title': 'Ihre Organisationen',
'organizations.list.description': 'Organisationen sind eine Möglichkeit, Ihre Dokumente zu gruppieren und den Zugriff darauf zu verwalten. Sie können mehrere Organisationen erstellen und Ihre Teammitglieder zur Zusammenarbeit einladen.',
'organizations.list.create-new': 'Neue Organisation erstellen',
'organizations.list.back': 'Zurück zu Organisationen',
'organizations.list.deleted.title': 'Gelöschte Organisationen',
'organizations.list.deleted.description': 'Gelöschte Organisationen werden für {{ days }} Tage aufbewahrt, bevor sie dauerhaft entfernt werden. Sie können sie während dieser Zeit wiederherstellen.',
'organizations.list.deleted.empty': 'Keine gelöschten Organisationen',
'organizations.list.deleted.empty-description': 'Wenn Sie eine Organisation löschen, wird sie hier für {{ days }} Tage angezeigt, bevor sie dauerhaft gelöscht wird.',
'organizations.list.deleted.restore': 'Wiederherstellen',
'organizations.list.deleted.restore-success': 'Organisation erfolgreich wiederhergestellt',
'organizations.list.deleted.restore-confirm.title': 'Organisation wiederherstellen',
'organizations.list.deleted.restore-confirm.message': 'Sind Sie sicher, dass Sie diese Organisation wiederherstellen möchten? Sie wird wieder in Ihre Liste der aktiven Organisationen verschoben.',
'organizations.list.deleted.restore-confirm.confirm-button': 'Organisation wiederherstellen',
'organizations.list.deleted.deleted-at': 'Gelöscht {{ date }}',
'organizations.list.deleted.purge-at': 'Wird dauerhaft gelöscht am {{ date }}',
'organizations.list.deleted.days-remaining': '({{ daysUntilPurge, =1:{daysUntilPurge} Tag, {daysUntilPurge} Tage }} verbleibend)',
'organizations.details.no-documents.title': 'Keine Dokumente',
'organizations.details.no-documents.description': 'Es sind noch keine Dokumente in dieser Organisation vorhanden. Beginnen Sie mit dem Hochladen von Dokumenten.',
'organizations.details.upload-documents': 'Dokumente hochladen',
'organizations.details.documents-count': 'Dokumente insgesamt',
'organizations.details.total-size': 'Gesamtgröße',
'organizations.details.latest-documents': 'Neueste importierte Dokumente',
'organizations.create.title': 'Eine neue Organisation erstellen',
'organizations.create.description': 'Ihre Dokumente werden nach Organisation gruppiert. Sie können mehrere Organisationen erstellen, um Ihre Dokumente zu trennen, z.B. für persönliche und geschäftliche Dokumente.',
'organizations.create.back': 'Zurück',
'organizations.create.error.max-count-reached': 'Sie haben die maximale Anzahl an Organisationen erreicht, die Sie erstellen können. Wenn Sie weitere erstellen möchten, kontaktieren Sie bitte den Support.',
'organizations.create.form.name.label': 'Name der Organisation',
'organizations.create.form.name.placeholder': 'Z.B. Acme Inc.',
'organizations.create.form.name.required': 'Bitte geben Sie einen Organisationsnamen ein',
'organizations.create.form.submit': 'Organisation erstellen',
'organizations.create.success': 'Organisation erfolgreich erstellt',
'organizations.create-first.title': 'Erstellen Sie Ihre Organisation',
'organizations.create-first.description': 'Ihre Dokumente werden nach Organisation gruppiert. Sie können mehrere Organisationen erstellen, um Ihre Dokumente zu trennen, z.B. für persönliche und geschäftliche Dokumente.',
'organizations.create-first.default-name': 'Meine Organisation',
'organizations.create-first.user-name': 'Organisation von "{{ name }}"',
'organization.settings.title': 'Organisationseinstellungen',
'organization.settings.page.title': 'Organisationseinstellungen',
'organization.settings.page.description': 'Verwalten Sie hier Ihre Organisationseinstellungen.',
'organization.settings.name.title': 'Name der Organisation',
'organization.settings.name.update': 'Namen aktualisieren',
'organization.settings.name.placeholder': 'Z.B. Acme Inc.',
'organization.settings.name.updated': 'Organisationsname aktualisiert',
'organization.settings.subscription.title': 'Abonnement',
'organization.settings.subscription.description': 'Verwalten Sie Ihre Abrechnung, Rechnungen und Zahlungsmethoden.',
'organization.settings.subscription.manage': 'Abonnement verwalten',
'organization.settings.subscription.error': 'Kundenportal-URL konnte nicht abgerufen werden',
'organization.settings.delete.title': 'Organisation löschen',
'organization.settings.delete.description': 'Das Löschen dieser Organisation entfernt dauerhaft alle damit verbundenen Daten.',
'organization.settings.delete.confirm.title': 'Organisation löschen',
'organization.settings.delete.confirm.message': 'Sind Sie sicher, dass Sie diese Organisation löschen möchten? Die Organisation wird zum Löschen markiert und nach {{ days }} Tagen endgültig entfernt. Während dieser Zeit können Sie sie aus Ihrer Organisationsliste wiederherstellen. Alle Dokumente und Daten werden nach dieser Frist dauerhaft gelöscht.',
'organization.settings.delete.confirm.confirm-button': 'Organisation löschen',
'organization.settings.delete.confirm.cancel-button': 'Abbrechen',
'organization.settings.delete.success': 'Organisation gelöscht',
'organization.settings.delete.only-owner': 'Nur der Organisationsinhaber kann diese Organisation löschen.',
'organization.usage.page.title': 'Nutzung',
'organization.usage.page.description': 'Sehen Sie die aktuelle Nutzung und Limits Ihrer Organisation.',
'organization.usage.storage.title': 'Dokumentenspeicher',
'organization.usage.storage.description': 'Gesamtspeicher, der von Ihren Dokumenten verwendet wird',
'organization.usage.intake-emails.title': 'Eingangs-E-Mails',
'organization.usage.intake-emails.description': 'Anzahl der Eingangs-E-Mail-Adressen',
'organization.usage.members.title': 'Mitglieder',
'organization.usage.members.description': 'Anzahl der Mitglieder in der Organisation',
'organization.usage.unlimited': 'Unbegrenzt',
'organizations.members.title': 'Mitglieder',
'organizations.members.description': 'Verwalten Sie Ihre Organisationsmitglieder',
'organizations.members.invite-member': 'Mitglied einladen',
'organizations.members.invite-member-disabled-tooltip': 'Nur Administratoren oder Eigentümer können Mitglieder in die Organisation einladen',
'organizations.members.remove-from-organization': 'Aus Organisation entfernen',
'organizations.members.role': 'Rolle',
'organizations.members.roles.owner': 'Eigentümer',
'organizations.members.roles.admin': 'Administrator',
'organizations.members.roles.member': 'Mitglied',
'organizations.members.delete.confirm.title': 'Mitglied entfernen',
'organizations.members.delete.confirm.message': 'Sind Sie sicher, dass Sie dieses Mitglied aus der Organisation entfernen möchten?',
'organizations.members.delete.confirm.confirm-button': 'Entfernen',
'organizations.members.delete.confirm.cancel-button': 'Abbrechen',
'organizations.members.delete.success': 'Mitglied aus Organisation entfernt',
'organizations.members.update-role.success': 'Mitgliederrolle aktualisiert',
'organizations.members.table.headers.name': 'Name',
'organizations.members.table.headers.email': 'E-Mail',
'organizations.members.table.headers.role': 'Rolle',
'organizations.members.table.headers.created': 'Erstellt',
'organizations.members.table.headers.actions': 'Aktionen',
'organizations.invite-member.title': 'Mitglied einladen',
'organizations.invite-member.description': 'Laden Sie ein Mitglied in Ihre Organisation ein',
'organizations.invite-member.form.email.label': 'E-Mail',
'organizations.invite-member.form.email.placeholder': 'Beispiel: ada@papra.app',
'organizations.invite-member.form.email.required': 'Bitte geben Sie eine gültige E-Mail-Adresse ein',
'organizations.invite-member.form.role.label': 'Rolle',
'organizations.invite-member.form.submit': 'In Organisation einladen',
'organizations.invite-member.success.message': 'Mitglied eingeladen',
'organizations.invite-member.success.description': 'Die E-Mail wurde in die Organisation eingeladen.',
'organizations.invite-member.error.message': 'Mitglied konnte nicht eingeladen werden',
'organizations.invitations.title': 'Einladungen',
'organizations.invitations.description': 'Verwalten Sie Ihre Organisationseinladungen',
'organizations.invitations.list.cta': 'Mitglied einladen',
'organizations.invitations.list.empty.title': 'Keine ausstehenden Einladungen',
'organizations.invitations.list.empty.description': 'Sie wurden noch nicht zu Organisationen eingeladen.',
'organizations.invitations.status.pending': 'Ausstehend',
'organizations.invitations.status.accepted': 'Angenommen',
'organizations.invitations.status.rejected': 'Abgelehnt',
'organizations.invitations.status.expired': 'Abgelaufen',
'organizations.invitations.status.cancelled': 'Abgebrochen',
'organizations.invitations.resend': 'Einladung erneut senden',
'organizations.invitations.cancel.title': 'Einladung abbrechen',
'organizations.invitations.cancel.description': 'Sind Sie sicher, dass Sie diese Einladung abbrechen möchten?',
'organizations.invitations.cancel.confirm': 'Einladung abbrechen',
'organizations.invitations.cancel.cancel': 'Abbrechen',
'organizations.invitations.resend.title': 'Einladung erneut senden',
'organizations.invitations.resend.description': 'Sind Sie sicher, dass Sie diese Einladung erneut senden möchten? Dadurch wird eine neue E-Mail an den Empfänger gesendet.',
'organizations.invitations.resend.confirm': 'Einladung erneut senden',
'organizations.invitations.resend.cancel': 'Abbrechen',
'invitations.list.title': 'Einladungen',
'invitations.list.description': 'Verwalten Sie Ihre Organisationseinladungen',
'invitations.list.empty.title': 'Keine ausstehenden Einladungen',
'invitations.list.empty.description': 'Sie wurden noch nicht zu Organisationen eingeladen.',
'invitations.list.headers.organization': 'Organisation',
'invitations.list.headers.status': 'Status',
'invitations.list.headers.created': 'Erstellt',
'invitations.list.headers.actions': 'Aktionen',
'invitations.list.actions.accept': 'Annehmen',
'invitations.list.actions.reject': 'Ablehnen',
'invitations.list.actions.accept.success.message': 'Einladung angenommen',
'invitations.list.actions.accept.success.description': 'Die Einladung wurde angenommen.',
'invitations.list.actions.reject.success.message': 'Einladung abgelehnt',
'invitations.list.actions.reject.success.description': 'Die Einladung wurde abgelehnt.',
// Documents
'documents.list.title': 'Dokumente',
'documents.list.no-documents.title': 'Keine Dokumente',
'documents.list.no-documents.description': 'Es sind noch keine Dokumente in dieser Organisation vorhanden. Beginnen Sie mit dem Hochladen von Dokumenten.',
'documents.list.no-results': 'Keine Dokumente gefunden',
'documents.tabs.info': 'Info',
'documents.tabs.content': 'Inhalt',
'documents.tabs.activity': 'Aktivität',
'documents.deleted.message': 'Dieses Dokument wurde gelöscht und wird in {{ days }} Tagen dauerhaft entfernt.',
'documents.actions.download': 'Herunterladen',
'documents.actions.open-in-new-tab': 'In neuem Tab öffnen',
'documents.actions.restore': 'Wiederherstellen',
'documents.actions.delete': 'Löschen',
'documents.actions.edit': 'Bearbeiten',
'documents.actions.cancel': 'Abbrechen',
'documents.actions.save': 'Speichern',
'documents.actions.saving': 'Speichern...',
'documents.content.alert': 'Der Inhalt des Dokuments wird beim Hochladen automatisch aus dem Dokument extrahiert. Er wird nur für Such- und Indexierungszwecke verwendet.',
'documents.content.empty-placeholder': 'Dieses Dokument hat keinen extrahierten Inhalt, Sie können ihn manuell hier eintragen.',
'documents.info.id': 'ID',
'documents.info.name': 'Name',
'documents.info.type': 'Typ',
'documents.info.size': 'Größe',
'documents.info.created-at': 'Erstellt am',
'documents.info.updated-at': 'Aktualisiert am',
'documents.info.never': 'Nie',
'documents.rename.title': 'Dokument umbenennen',
'documents.rename.form.name.label': 'Name',
'documents.rename.form.name.placeholder': 'Beispiel: Rechnung 2024',
'documents.rename.form.name.required': 'Bitte geben Sie einen Namen für das Dokument ein',
'documents.rename.form.name.max-length': 'Der Name muss weniger als 255 Zeichen lang sein',
'documents.rename.form.submit': 'Dokument umbenennen',
'documents.rename.success': 'Dokument erfolgreich umbenannt',
'documents.rename.cancel': 'Abbrechen',
'import-documents.title.error': '{{ count }} Dokumente fehlgeschlagen',
'import-documents.title.success': '{{ count }} Dokumente importiert',
'import-documents.title.pending': '{{ count }} / {{ total }} Dokumente importiert',
'import-documents.title.none': 'Dokumente importieren',
'import-documents.no-import-in-progress': 'Kein Dokumentimport im Gange',
'documents.deleted.title': 'Gelöschte Dokumente',
'documents.deleted.empty.title': 'Keine gelöschten Dokumente',
'documents.deleted.empty.description': 'Sie haben keine gelöschten Dokumente. Gelöschte Dokumente werden für {{ days }} Tage in den Papierkorb verschoben.',
'documents.deleted.retention-notice': 'Alle gelöschten Dokumente werden für {{ days }} Tage im Papierkorb gespeichert. Nach Ablauf dieser Frist werden die Dokumente dauerhaft gelöscht und Sie können sie nicht wiederherstellen.',
'documents.deleted.deleted-at': 'Gelöscht',
'documents.deleted.restoring': 'Wiederherstellen...',
'documents.deleted.deleting': 'Löschen...',
'documents.preview.unknown-file-type': 'Kein Vorschau verfügbar für diesen Dateityp',
'documents.preview.binary-file': 'Dies scheint eine Binärdatei zu sein und kann nicht als Text angezeigt werden',
'trash.delete-all.button': 'Alles löschen',
'trash.delete-all.confirm.title': 'Alle Dokumente dauerhaft löschen?',
'trash.delete-all.confirm.description': 'Sind Sie sicher, dass Sie alle Dokumente aus dem Papierkorb dauerhaft löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.',
'trash.delete-all.confirm.label': 'Löschen',
'trash.delete-all.confirm.cancel': 'Abbrechen',
'trash.delete.button': 'Löschen',
'trash.delete.confirm.title': 'Dokument dauerhaft löschen?',
'trash.delete.confirm.description': 'Sind Sie sicher, dass Sie dieses Dokument dauerhaft aus dem Papierkorb löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.',
'trash.delete.confirm.label': 'Löschen',
'trash.delete.confirm.cancel': 'Abbrechen',
'trash.deleted.success.title': 'Dokument gelöscht',
'trash.deleted.success.description': 'Das Dokument wurde dauerhaft gelöscht.',
'activity.document.created': 'Das Dokument wurde erstellt',
'activity.document.updated.single': 'Das Feld {{ field }} wurde aktualisiert',
'activity.document.updated.multiple': 'Die Felder {{ fields }} wurden aktualisiert',
'activity.document.updated': 'Das Dokument wurde aktualisiert',
'activity.document.deleted': 'Das Dokument wurde gelöscht',
'activity.document.restored': 'Das Dokument wurde wiederhergestellt',
'activity.document.tagged': 'Tag {{ tag }} wurde hinzugefügt',
'activity.document.untagged': 'Tag {{ tag }} wurde entfernt',
'activity.document.user.name': 'von {{ name }}',
'activity.load-more': 'Mehr laden',
'activity.no-more-activities': 'Keine weiteren Aktivitäten für dieses Dokument',
// Tags
'tags.no-tags.title': 'Noch keine Tags',
'tags.no-tags.description': 'Diese Organisation hat noch keine Tags. Tags werden zur Kategorisierung von Dokumenten verwendet. Sie können Ihren Dokumenten Tags hinzufügen, um sie leichter zu finden und zu organisieren.',
'tags.no-tags.create-tag': 'Tag erstellen',
'tags.title': 'Dokumenten-Tags',
'tags.description': 'Tags werden zur Kategorisierung von Dokumenten verwendet. Sie können Ihren Dokumenten Tags hinzufügen, um sie leichter zu finden und zu organisieren.',
'tags.create': 'Tag erstellen',
'tags.update': 'Tag aktualisieren',
'tags.delete': 'Tag löschen',
'tags.delete.confirm.title': 'Tag löschen',
'tags.delete.confirm.message': 'Sind Sie sicher, dass Sie diesen Tag löschen möchten? Das Löschen eines Tags entfernt ihn von allen Dokumenten.',
'tags.delete.confirm.confirm-button': 'Löschen',
'tags.delete.confirm.cancel-button': 'Abbrechen',
'tags.delete.success': 'Tag erfolgreich gelöscht',
'tags.create.success': 'Tag "{{ name }}" erfolgreich erstellt.',
'tags.update.success': 'Tag "{{ name }}" erfolgreich aktualisiert.',
'tags.form.name.label': 'Name',
'tags.form.name.placeholder': 'Z.B. Verträge',
'tags.form.name.required': 'Bitte geben Sie einen Tag-Namen ein',
'tags.form.name.max-length': 'Tag-Name muss weniger als 64 Zeichen lang sein',
'tags.form.color.label': 'Farbe',
'tags.form.color.required': 'Bitte geben Sie eine Farbe ein',
'tags.form.color.invalid': 'Die Hex-Farbe ist falsch formatiert.',
'tags.form.description.label': 'Beschreibung',
'tags.form.description.optional': '(optional)',
'tags.form.description.placeholder': 'Z.B. Alle von der Firma unterzeichneten Verträge',
'tags.form.description.max-length': 'Beschreibung muss weniger als 256 Zeichen lang sein',
'tags.form.no-description': 'Keine Beschreibung',
'tags.table.headers.tag': 'Tag',
'tags.table.headers.description': 'Beschreibung',
'tags.table.headers.documents': 'Dokumente',
'tags.table.headers.created': 'Erstellt',
'tags.table.headers.actions': 'Aktionen',
// Tagging rules
'tagging-rules.field.name': 'Dokumentenname',
'tagging-rules.field.content': 'Dokumenteninhalt',
'tagging-rules.operator.equals': 'ist gleich',
'tagging-rules.operator.not-equals': 'ist nicht gleich',
'tagging-rules.operator.contains': 'enthält',
'tagging-rules.operator.not-contains': 'enthält nicht',
'tagging-rules.operator.starts-with': 'beginnt mit',
'tagging-rules.operator.ends-with': 'endet mit',
'tagging-rules.list.title': 'Tagging-Regeln',
'tagging-rules.list.description': 'Verwalten Sie die Tagging-Regeln Ihrer Organisation, um Dokumente automatisch basierend auf von Ihnen definierten Bedingungen zu taggen.',
'tagging-rules.list.demo-warning': 'Hinweis: Da dies eine Demo-Umgebung (ohne Server) ist, werden Tagging-Regeln nicht auf neu hinzugefügte Dokumente angewendet.',
'tagging-rules.list.no-tagging-rules.title': 'Keine Tagging-Regeln',
'tagging-rules.list.no-tagging-rules.description': 'Erstellen Sie eine Tagging-Regel, um Ihre hinzugefügten Dokumente automatisch basierend auf von Ihnen definierten Bedingungen zu taggen.',
'tagging-rules.list.no-tagging-rules.create-tagging-rule': 'Tagging-Regel erstellen',
'tagging-rules.list.card.no-conditions': 'Keine Bedingungen',
'tagging-rules.list.card.one-condition': '1 Bedingung',
'tagging-rules.list.card.conditions': '{{ count }} Bedingungen',
'tagging-rules.list.card.delete': 'Regel löschen',
'tagging-rules.list.card.edit': 'Regel bearbeiten',
'tagging-rules.create.title': 'Tagging-Regel erstellen',
'tagging-rules.create.success': 'Tagging-Regel erfolgreich erstellt',
'tagging-rules.create.error': 'Tagging-Regel konnte nicht erstellt werden',
'tagging-rules.create.submit': 'Regel erstellen',
'tagging-rules.form.name.label': 'Name',
'tagging-rules.form.name.placeholder': 'Beispiel: Rechnungen taggen',
'tagging-rules.form.name.min-length': 'Bitte geben Sie einen Namen für die Regel ein',
'tagging-rules.form.name.max-length': 'Der Name muss weniger als 64 Zeichen lang sein',
'tagging-rules.form.description.label': 'Beschreibung',
'tagging-rules.form.description.placeholder': 'Beispiel: Dokumente mit \'Rechnung\' im Namen taggen',
'tagging-rules.form.description.max-length': 'Die Beschreibung muss weniger als 256 Zeichen lang sein',
'tagging-rules.form.conditions.label': 'Bedingungen',
'tagging-rules.form.conditions.description': 'Definieren Sie die Bedingungen, die erfüllt sein müssen, damit die Regel angewendet wird. Alle Bedingungen müssen erfüllt sein, damit die Regel angewendet wird.',
'tagging-rules.form.conditions.add-condition': 'Bedingung hinzufügen',
'tagging-rules.form.conditions.no-conditions.title': 'Keine Bedingungen',
'tagging-rules.form.conditions.no-conditions.description': 'Sie haben dieser Regel keine Bedingungen hinzugefügt. Diese Regel wendet ihre Tags auf alle Dokumente an.',
'tagging-rules.form.conditions.no-conditions.confirm': 'Regel ohne Bedingungen anwenden',
'tagging-rules.form.conditions.no-conditions.cancel': 'Abbrechen',
'tagging-rules.form.conditions.value.placeholder': 'Beispiel: Rechnung',
'tagging-rules.form.conditions.value.min-length': 'Bitte geben Sie einen Wert für die Bedingung ein',
'tagging-rules.form.tags.label': 'Tags',
'tagging-rules.form.tags.description': 'Wählen Sie die Tags aus, die auf die hinzugefügten Dokumente angewendet werden sollen, die den Bedingungen entsprechen',
'tagging-rules.form.tags.min-length': 'Es ist mindestens ein anzuwendender Tag erforderlich',
'tagging-rules.form.tags.add-tag': 'Tag erstellen',
'tagging-rules.form.submit': 'Regel erstellen',
'tagging-rules.update.title': 'Tagging-Regel aktualisieren',
'tagging-rules.update.error': 'Tagging-Regel konnte nicht aktualisiert werden',
'tagging-rules.update.submit': 'Regel aktualisieren',
'tagging-rules.update.cancel': 'Abbrechen',
// Intake emails
'intake-emails.title': 'E-Mail-Eingang',
'intake-emails.description': 'E-Mail-Eingangsadressen werden verwendet, um E-Mails automatisch in Papra aufzunehmen. Leiten Sie einfach E-Mails an die Eingangsadresse weiter und deren Anhänge werden zu den Dokumenten Ihrer Organisation hinzugefügt.',
'intake-emails.disabled.title': 'E-Mail-Eingang ist deaktiviert',
'intake-emails.disabled.description': 'E-Mail-Eingang ist auf dieser Instanz deaktiviert. Bitte kontaktieren Sie Ihren Administrator, um ihn zu aktivieren. Weitere Informationen finden Sie in der {{ documentation }}.',
'intake-emails.disabled.documentation': 'Dokumentation',
'intake-emails.info': 'Es werden nur aktivierte E-Mails aus zulässigen Ursprüngen verarbeitet. Sie können eine E-Mail-Eingangsadresse jederzeit aktivieren oder deaktivieren.',
'intake-emails.empty.title': 'Keine E-Mail-Eingänge',
'intake-emails.empty.description': 'Generieren Sie eine Eingangsadresse, um E-Mail-Anhänge einfach aufzunehmen.',
'intake-emails.empty.generate': 'E-Mail-Eingang generieren',
'intake-emails.count': '{{ count }} Eingangse-Mail{{ plural }} für diese Organisation',
'intake-emails.new': 'Neue Eingangse-Mail',
'intake-emails.disabled-label': '(Deaktiviert)',
'intake-emails.no-origins': 'Keine zulässigen E-Mail-Ursprünge',
'intake-emails.allowed-origins': 'Zulässig von {{ count }} Adresse{{ plural }}',
'intake-emails.actions.enable': 'Aktivieren',
'intake-emails.actions.disable': 'Deaktivieren',
'intake-emails.actions.manage-origins': 'Ursprungsadressen verwalten',
'intake-emails.actions.delete': 'Löschen',
'intake-emails.delete.confirm.title': 'Eingangse-Mail löschen?',
'intake-emails.delete.confirm.message': 'Sind Sie sicher, dass Sie diese Eingangse-Mail löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.',
'intake-emails.delete.confirm.confirm-button': 'Eingangse-Mail löschen',
'intake-emails.delete.confirm.cancel-button': 'Abbrechen',
'intake-emails.delete.success': 'Eingangse-Mail gelöscht',
'intake-emails.create.success': 'Eingangse-Mail erstellt',
'intake-emails.update.success.enabled': 'Eingangse-Mail aktiviert',
'intake-emails.update.success.disabled': 'Eingangse-Mail deaktiviert',
'intake-emails.allowed-origins.title': 'Zulässige Ursprünge',
'intake-emails.allowed-origins.description': 'Es werden nur E-Mails, die an {{ email }} von diesen Ursprüngen gesendet werden, verarbeitet. Wenn keine Ursprünge angegeben sind, werden alle E-Mails verworfen.',
'intake-emails.allowed-origins.add.label': 'Zulässige Ursprungs-E-Mail hinzufügen',
'intake-emails.allowed-origins.add.placeholder': 'Z.B. ada@papra.app',
'intake-emails.allowed-origins.add.button': 'Hinzufügen',
'intake-emails.allowed-origins.add.error.exists': 'Diese E-Mail ist bereits in den zulässigen Ursprüngen für diese Eingangse-Mail vorhanden',
// API keys
'api-keys.permissions.select-all': 'Alle auswählen',
'api-keys.permissions.deselect-all': 'Alle abwählen',
'api-keys.permissions.organizations.title': 'Organisationen',
'api-keys.permissions.organizations.organizations:create': 'Organisationen erstellen',
'api-keys.permissions.organizations.organizations:read': 'Organisationen lesen',
'api-keys.permissions.organizations.organizations:update': 'Organisationen aktualisieren',
'api-keys.permissions.organizations.organizations:delete': 'Organisationen löschen',
'api-keys.permissions.documents.title': 'Dokumente',
'api-keys.permissions.documents.documents:create': 'Dokumente erstellen',
'api-keys.permissions.documents.documents:read': 'Dokumente lesen',
'api-keys.permissions.documents.documents:update': 'Dokumente aktualisieren',
'api-keys.permissions.documents.documents:delete': 'Dokumente löschen',
'api-keys.permissions.tags.title': 'Tags',
'api-keys.permissions.tags.tags:create': 'Tags erstellen',
'api-keys.permissions.tags.tags:read': 'Tags lesen',
'api-keys.permissions.tags.tags:update': 'Tags aktualisieren',
'api-keys.permissions.tags.tags:delete': 'Tags löschen',
'api-keys.create.title': 'API-Schlüssel erstellen',
'api-keys.create.description': 'Erstellen Sie einen neuen API-Schlüssel, um auf die Papra API zuzugreifen.',
'api-keys.create.success': 'Der API-Schlüssel wurde erfolgreich erstellt.',
'api-keys.create.back': 'Zurück zu den API-Schlüsseln',
'api-keys.create.form.name.label': 'Name',
'api-keys.create.form.name.placeholder': 'Beispiel: Mein API-Schlüssel',
'api-keys.create.form.name.required': 'Bitte geben Sie einen Namen für den API-Schlüssel ein',
'api-keys.create.form.permissions.label': 'Berechtigungen',
'api-keys.create.form.permissions.required': 'Bitte wählen Sie mindestens eine Berechtigung aus',
'api-keys.create.form.submit': 'API-Schlüssel erstellen',
'api-keys.create.created.title': 'API-Schlüssel erstellt',
'api-keys.create.created.description': 'Der API-Schlüssel wurde erfolgreich erstellt. Speichern Sie ihn an einem sicheren Ort, da er nicht erneut angezeigt wird.',
'api-keys.list.title': 'API-Schlüssel',
'api-keys.list.description': 'Verwalten Sie hier Ihre API-Schlüssel.',
'api-keys.list.create': 'API-Schlüssel erstellen',
'api-keys.list.empty.title': 'Keine API-Schlüssel',
'api-keys.list.empty.description': 'Erstellen Sie einen API-Schlüssel, um auf die Papra API zuzugreifen.',
'api-keys.list.card.last-used': 'Zuletzt verwendet',
'api-keys.list.card.never': 'Nie',
'api-keys.list.card.created': 'Erstellt',
'api-keys.delete.success': 'Der API-Schlüssel wurde erfolgreich gelöscht',
'api-keys.delete.confirm.title': 'API-Schlüssel löschen',
'api-keys.delete.confirm.message': 'Sind Sie sicher, dass Sie diesen API-Schlüssel löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.',
'api-keys.delete.confirm.confirm-button': 'Löschen',
'api-keys.delete.confirm.cancel-button': 'Abbrechen',
// Webhooks
'webhooks.list.title': 'Webhooks',
'webhooks.list.description': 'Verwalten Sie Ihre Organisations-Webhooks',
'webhooks.list.empty.title': 'Keine Webhooks',
'webhooks.list.empty.description': 'Erstellen Sie Ihren ersten Webhook, um Ereignisse zu empfangen',
'webhooks.list.create': 'Webhook erstellen',
'webhooks.list.card.last-triggered': 'Zuletzt ausgelöst',
'webhooks.list.card.never': 'Nie',
'webhooks.list.card.created': 'Erstellt',
'webhooks.create.title': 'Webhook erstellen',
'webhooks.create.description': 'Erstellen Sie einen neuen Webhook, um Ereignisse zu empfangen',
'webhooks.create.success': 'Webhook erfolgreich erstellt',
'webhooks.create.back': 'Zurück',
'webhooks.create.form.submit': 'Webhook erstellen',
'webhooks.create.form.name.label': 'Webhook-Name',
'webhooks.create.form.name.placeholder': 'Webhook-Namen eingeben',
'webhooks.create.form.name.required': 'Name ist erforderlich',
'webhooks.create.form.url.label': 'Webhook-URL',
'webhooks.create.form.url.placeholder': 'Webhook-URL eingeben',
'webhooks.create.form.url.required': 'URL ist erforderlich',
'webhooks.create.form.url.invalid': 'URL ist ungültig',
'webhooks.create.form.secret.label': 'Geheimnis',
'webhooks.create.form.secret.placeholder': 'Webhook-Geheimnis eingeben',
'webhooks.create.form.events.label': 'Ereignisse',
'webhooks.create.form.events.required': 'Mindestens ein Ereignis ist erforderlich',
'webhooks.update.title': 'Webhook bearbeiten',
'webhooks.update.description': 'Aktualisieren Sie Ihre Webhook-Details',
'webhooks.update.success': 'Webhook erfolgreich aktualisiert',
'webhooks.update.submit': 'Webhook aktualisieren',
'webhooks.update.cancel': 'Abbrechen',
'webhooks.update.form.secret.placeholder': 'Neues Geheimnis eingeben',
'webhooks.update.form.secret.placeholder-redacted': '[Geheimnis geschwärzt]',
'webhooks.update.form.rotate-secret.button': 'Geheimnis rotieren',
'webhooks.delete.success': 'Webhook erfolgreich gelöscht',
'webhooks.delete.confirm.title': 'Webhook löschen',
'webhooks.delete.confirm.message': 'Sind Sie sicher, dass Sie diesen Webhook löschen möchten?',
'webhooks.delete.confirm.confirm-button': 'Löschen',
'webhooks.delete.confirm.cancel-button': 'Abbrechen',
'webhooks.events.documents.title': 'Dokumente Ereignisse',
'webhooks.events.documents.document:created.description': 'Dokument erstellt',
'webhooks.events.documents.document:deleted.description': 'Dokument gelöscht',
'webhooks.events.documents.document:updated.description': 'Dokument aktualisiert',
'webhooks.events.documents.document:tag:added.description': 'Ein Tag wurde zu einem Dokument hinzugefügt',
'webhooks.events.documents.document:tag:removed.description': 'Ein Tag wurde von einem Dokument entfernt',
// Navigation
'layout.menu.home': 'Startseite',
'layout.menu.documents': 'Dokumente',
'layout.menu.tags': 'Tags',
'layout.menu.tagging-rules': 'Tagging-Regeln',
'layout.menu.deleted-documents': 'Gelöschte Dokumente',
'layout.menu.organization-settings': 'Einstellungen',
'layout.menu.api-keys': 'API-Schlüssel',
'layout.menu.settings': 'Einstellungen',
'layout.menu.account': 'Konto',
'layout.menu.general-settings': 'Allgemeine Einstellungen',
'layout.menu.usage': 'Nutzung',
'layout.menu.intake-emails': 'E-Mail-Eingang',
'layout.menu.webhooks': 'Webhooks',
'layout.menu.members': 'Mitglieder',
'layout.menu.invitations': 'Einladungen',
'layout.upgrade-cta.title': 'Brauchen Sie mehr Platz?',
'layout.upgrade-cta.description': '10x mehr Speicher + Team-Zusammenarbeit',
'layout.upgrade-cta.button': 'Jetzt upgraden',
'layout.theme.light': 'Heller Modus',
'layout.theme.dark': 'Dunkler Modus',
'layout.theme.system': 'Systemmodus',
'layout.search.placeholder': 'Suchen...',
'layout.menu.import-document': 'Dokument importieren',
'user-menu.account-settings': 'Kontoeinstellungen',
'user-menu.api-keys': 'API-Schlüssel',
'user-menu.invitations': 'Einladungen',
'user-menu.language': 'Sprache',
'user-menu.logout': 'Abmelden',
// Command palette
'command-palette.search.placeholder': 'Befehle oder Dokumente suchen',
'command-palette.no-results': 'Keine Ergebnisse gefunden',
'command-palette.sections.documents': 'Dokumente',
'command-palette.sections.theme': 'Thema',
// API errors
'api-errors.document.already_exists': 'Das Dokument existiert bereits',
'api-errors.document.size_too_large': 'Die Datei ist zu groß',
'api-errors.intake-emails.already_exists': 'Eine Eingang-Email mit dieser Adresse existiert bereits.',
'api-errors.intake_email.limit_reached': 'Die maximale Anzahl an Eingang-EMails für diese Organisation wurde erreicht. Bitte aktualisieren Sie Ihren Plan, um weitere Eingangse-Mails zu erstellen.',
'api-errors.user.max_organization_count_reached': 'Sie haben die maximale Anzahl an Organisationen erreicht, die Sie erstellen können. Wenn Sie weitere erstellen möchten, kontaktieren Sie bitte den Support.',
'api-errors.default': 'Beim Verarbeiten Ihrer Anfrage ist ein Fehler aufgetreten.',
'api-errors.organization.invitation_already_exists': 'Eine Einladung für diese E-Mail existiert bereits in dieser Organisation.',
'api-errors.user.already_in_organization': 'Dieser Benutzer ist bereits in dieser Organisation.',
'api-errors.user.organization_invitation_limit_reached': 'Die maximale Anzahl an Einladungen für heute wurde erreicht. Bitte versuchen Sie es morgen erneut.',
'api-errors.demo.not_available': 'Diese Funktion ist in der Demo nicht verfügbar',
'api-errors.tags.already_exists': 'Ein Tag mit diesem Namen existiert bereits für diese Organisation',
'api-errors.internal.error': 'Beim Verarbeiten Ihrer Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.',
'api-errors.auth.invalid_origin': 'Ungültige Anwendungs-Ursprung. Wenn Sie Papra selbst hosten, stellen Sie sicher, dass Ihre APP_BASE_URL-Umgebungsvariable mit Ihrer aktuellen URL übereinstimmt. Weitere Details finden Sie unter https://docs.papra.app/resources/troubleshooting/#invalid-application-origin',
'api-errors.organization.max_members_count_reached': 'Die maximale Anzahl an Mitgliedern und ausstehenden Einladungen für diese Organisation wurde erreicht. Bitte aktualisieren Sie Ihren Plan, um weitere Mitglieder hinzuzufügen.',
// Not found
'not-found.title': '404 - Seite nicht gefunden',
'not-found.description': 'Entschuldigung, die gesuchte Seite scheint nicht zu existieren. Bitte überprüfen Sie die URL und versuchen Sie es erneut.',
'not-found.back-to-home': 'Zurück zur Startseite',
// Demo
'demo.popup.description': 'Dies ist eine Demo-Umgebung, alle Daten werden im lokalen Speicher Ihres Browsers gespeichert.',
'demo.popup.discord': 'Treten Sie dem {{ discordLink }} bei, um Support zu erhalten, Funktionen vorzuschlagen oder einfach nur zu chatten.',
'demo.popup.discord-link-label': 'Discord-Server',
'demo.popup.reset': 'Demo-Daten zurücksetzen',
'demo.popup.hide': 'Ausblenden',
// Color picker
'color-picker.hue': 'Farbton',
'color-picker.saturation': 'Sättigung',
'color-picker.lightness': 'Helligkeit',
'color-picker.select-color': 'Farbe auswählen',
'color-picker.select-a-color': 'Eine Farbe auswählen',
// Subscriptions
'subscriptions.checkout-success.title': 'Zahlung erfolgreich!',
'subscriptions.checkout-success.description': 'Ihr Abonnement wurde erfolgreich aktiviert.',
'subscriptions.checkout-success.thank-you': 'Vielen Dank für Ihr Upgrade auf Papra Plus. Sie haben jetzt Zugriff auf alle Premium-Funktionen.',
'subscriptions.checkout-success.go-to-organizations': 'Zu Organisationen',
'subscriptions.checkout-success.redirecting': 'Weiterleitung in {{ count }} Sekunde{{ plural }}...',
'subscriptions.checkout-cancel.title': 'Zahlung abgebrochen',
'subscriptions.checkout-cancel.description': 'Ihr Abonnement-Upgrade wurde abgebrochen.',
'subscriptions.checkout-cancel.no-charges': 'Es wurden keine Gebühren von Ihrem Konto abgebucht. Sie können es jederzeit erneut versuchen.',
'subscriptions.checkout-cancel.back-to-organizations': 'Zurück zu Organisationen',
'subscriptions.checkout-cancel.need-help': 'Benötigen Sie Hilfe?',
'subscriptions.checkout-cancel.contact-support': 'Support kontaktieren',
'subscriptions.upgrade-dialog.title': 'Diese Organisation upgraden',
'subscriptions.upgrade-dialog.description': 'Schalten Sie leistungsstarke Funktionen für Ihre Organisation frei',
'subscriptions.upgrade-dialog.contact-us': 'Kontaktieren Sie uns',
'subscriptions.upgrade-dialog.enterprise-plans': 'wenn Sie benutzerdefinierte Enterprise-Pläne benötigen.',
'subscriptions.upgrade-dialog.current-plan': 'Aktueller Plan',
'subscriptions.upgrade-dialog.recommended': 'Empfohlen',
'subscriptions.upgrade-dialog.per-month': '/Monat',
'subscriptions.upgrade-dialog.billed-annually': '${{ price }} jährlich abgerechnet',
'subscriptions.upgrade-dialog.upgrade-now': 'Jetzt upgraden',
'subscriptions.plan.free.name': 'Kostenloser Plan',
'subscriptions.plan.plus.name': 'Plus',
'subscriptions.plan.pro.name': 'Pro',
'subscriptions.features.storage-size': 'Dokumentenspeichergröße',
'subscriptions.features.members': 'Organisationsmitglieder',
'subscriptions.features.members-count': '{{ count }} Mitglieder',
'subscriptions.features.email-intakes': 'E-Mail-Eingänge',
'subscriptions.features.email-intakes-count-singular': '{{ count }} Adresse',
'subscriptions.features.email-intakes-count-plural': '{{ count }} Adressen',
'subscriptions.features.max-upload-size': 'Maximale Upload-Dateigröße',
'subscriptions.features.support': 'Support',
'subscriptions.features.support-community': 'Community-Support',
'subscriptions.features.support-email': 'E-Mail-Support',
'subscriptions.features.support-priority': 'Prioritäts-Support',
'subscriptions.billing-interval.monthly': 'Monatlich',
'subscriptions.billing-interval.annual': 'Jährlich',
'subscriptions.usage-warning.message': 'Sie haben {{ percent }}% Ihres Dokumentenspeichers verwendet. Erwägen Sie ein Upgrade Ihres Plans, um mehr Speicherplatz zu erhalten.',
'subscriptions.usage-warning.upgrade-button': 'Plan upgraden',
// Common / Shared
'common.confirm-modal.type-to-confirm': 'Geben Sie "{{ text }}" ein zur Bestätigung',
};

View File

@@ -0,0 +1,663 @@
export const translations = {
// Authentication
'auth.request-password-reset.title': 'Reset your password',
'auth.request-password-reset.description': 'Enter your email to reset your password.',
'auth.request-password-reset.requested': 'If an account exists for this email, we\'ve sent you an email to reset your password.',
'auth.request-password-reset.back-to-login': 'Back to login',
'auth.request-password-reset.form.email.label': 'Email',
'auth.request-password-reset.form.email.placeholder': 'Example: ada@papra.app',
'auth.request-password-reset.form.email.required': 'Please enter your email address',
'auth.request-password-reset.form.email.invalid': 'This email address is invalid',
'auth.request-password-reset.form.submit': 'Request password reset',
'auth.reset-password.title': 'Reset your password',
'auth.reset-password.description': 'Enter your new password to reset your password.',
'auth.reset-password.reset': 'Your password has been reset.',
'auth.reset-password.back-to-login': 'Back to login',
'auth.reset-password.form.new-password.label': 'New password',
'auth.reset-password.form.new-password.placeholder': 'Example: **********',
'auth.reset-password.form.new-password.required': 'Please enter your new password',
'auth.reset-password.form.new-password.min-length': 'Password must be at least {{ minLength }} characters',
'auth.reset-password.form.new-password.max-length': 'Password must be less than {{ maxLength }} characters',
'auth.reset-password.form.submit': 'Reset password',
'auth.email-provider.open': 'Open {{ provider }}',
'auth.login.title': 'Login to Papra',
'auth.login.description': 'Enter your email or use social login to access your Papra account.',
'auth.login.login-with-provider': 'Login with {{ provider }}',
'auth.login.no-account': 'Don\'t have an account?',
'auth.login.register': 'Register',
'auth.login.form.email.label': 'Email',
'auth.login.form.email.placeholder': 'Example: ada@papra.app',
'auth.login.form.email.required': 'Please enter your email address',
'auth.login.form.email.invalid': 'This email address is invalid',
'auth.login.form.password.label': 'Password',
'auth.login.form.password.placeholder': 'Set a password',
'auth.login.form.password.required': 'Please enter your password',
'auth.login.form.remember-me.label': 'Remember me',
'auth.login.form.forgot-password.label': 'Forgot password?',
'auth.login.form.submit': 'Login',
'auth.register.title': 'Register to Papra',
'auth.register.description': 'Create an account to start using Papra.',
'auth.register.register-with-email': 'Register with email',
'auth.register.register-with-provider': 'Register with {{ provider }}',
'auth.register.providers.google': 'Google',
'auth.register.providers.github': 'GitHub',
'auth.register.have-account': 'Already have an account?',
'auth.register.login': 'Login',
'auth.register.registration-disabled.title': 'Registration is disabled',
'auth.register.registration-disabled.description': 'The creation of new accounts is currently disabled on this instance of Papra. Only users with existing accounts can log in. If you think this is a mistake, please contact the administrator of this instance.',
'auth.register.form.email.label': 'Email',
'auth.register.form.email.placeholder': 'Example: ada@papra.app',
'auth.register.form.email.required': 'Please enter your email address',
'auth.register.form.email.invalid': 'This email address is invalid',
'auth.register.form.password.label': 'Password',
'auth.register.form.password.placeholder': 'Set a password',
'auth.register.form.password.required': 'Please enter your password',
'auth.register.form.password.min-length': 'Password must be at least {{ minLength }} characters',
'auth.register.form.password.max-length': 'Password must be less than {{ maxLength }} characters',
'auth.register.form.name.label': 'Name',
'auth.register.form.name.placeholder': 'Example: Ada Lovelace',
'auth.register.form.name.required': 'Please enter your name',
'auth.register.form.name.max-length': 'Name must be less than {{ maxLength }} characters',
'auth.register.form.submit': 'Register',
'auth.email-validation-required.title': 'Verify your email',
'auth.email-validation-required.description': 'A verification email has been sent to your email address. Please verify your email address by clicking the link in the email.',
'auth.legal-links.description': 'By continuing, you acknowledge that you understand and agree to the {{ terms }} and {{ privacy }}.',
'auth.legal-links.terms': 'Terms of Service',
'auth.legal-links.privacy': 'Privacy Policy',
'auth.no-auth-provider.title': 'No authentication provider',
'auth.no-auth-provider.description': 'There are no authentication providers enabled on this instance of Papra. Please contact the administrator of this instance to enable them.',
// User settings
'user.settings.title': 'User settings',
'user.settings.description': 'Manage your account settings here.',
'user.settings.email.title': 'Email address',
'user.settings.email.description': 'Your email address cannot be changed.',
'user.settings.email.label': 'Email address',
'user.settings.name.title': 'Full name',
'user.settings.name.description': 'Your full name is displayed to other organization members.',
'user.settings.name.label': 'Full name',
'user.settings.name.placeholder': 'Eg. John Doe',
'user.settings.name.update': 'Update name',
'user.settings.name.updated': 'Your full name has been updated',
'user.settings.logout.title': 'Logout',
'user.settings.logout.description': 'Logout from your account. You can login again later.',
'user.settings.logout.button': 'Logout',
// Organizations
'organizations.list.title': 'Your organizations',
'organizations.list.description': 'Organizations are a way to group your documents and manage access to them. You can create multiple organizations and invite your team members to collaborate.',
'organizations.list.create-new': 'Create new organization',
'organizations.list.back': 'Back to organizations',
'organizations.list.deleted.title': 'Deleted organizations',
'organizations.list.deleted.description': 'Deleted organizations are kept for {{ days }} days before being permanently removed. You can restore them during this period.',
'organizations.list.deleted.empty': 'No deleted organizations',
'organizations.list.deleted.empty-description': 'When you delete an organization, it will appear here for {{ days }} days before being permanently deleted.',
'organizations.list.deleted.restore': 'Restore',
'organizations.list.deleted.restore-success': 'Organization restored successfully',
'organizations.list.deleted.restore-confirm.title': 'Restore organization',
'organizations.list.deleted.restore-confirm.message': 'Are you sure you want to restore this organization? It will be moved back to your active organizations list.',
'organizations.list.deleted.restore-confirm.confirm-button': 'Restore organization',
'organizations.list.deleted.deleted-at': 'Deleted {{ date }}',
'organizations.list.deleted.purge-at': 'Will be permanently deleted on {{ date }}',
'organizations.list.deleted.days-remaining': '({{ daysUntilPurge, =1:{daysUntilPurge} day, {daysUntilPurge} days }} remaining)',
'organizations.details.no-documents.title': 'No documents',
'organizations.details.no-documents.description': 'There are no documents in this organization yet. Start by uploading some documents.',
'organizations.details.upload-documents': 'Upload documents',
'organizations.details.documents-count': 'documents in total',
'organizations.details.total-size': 'total size',
'organizations.details.latest-documents': 'Latest imported documents',
'organizations.create.title': 'Create a new organization',
'organizations.create.description': 'Your documents will be grouped by organization. You can create multiple organizations to separate your documents, for example, for personal and work documents.',
'organizations.create.back': 'Back',
'organizations.create.error.max-count-reached': 'You have reached the maximum number of organizations you can create, if you need to create more, please contact support.',
'organizations.create.form.name.label': 'Organization name',
'organizations.create.form.name.placeholder': 'Eg. Acme Inc.',
'organizations.create.form.name.required': 'Please enter an organization name',
'organizations.create.form.submit': 'Create organization',
'organizations.create.success': 'Organization created successfully',
'organizations.create-first.title': 'Create your organization',
'organizations.create-first.description': 'Your documents will be grouped by organization. You can create multiple organizations to separate your documents, for example, for personal and work documents.',
'organizations.create-first.default-name': 'My organization',
'organizations.create-first.user-name': '{{ name }}\'s organization',
'organization.settings.title': 'Organization Settings',
'organization.settings.page.title': 'Organization settings',
'organization.settings.page.description': 'Manage your organization settings here.',
'organization.settings.name.title': 'Organization name',
'organization.settings.name.update': 'Update name',
'organization.settings.name.placeholder': 'Eg. Acme Inc.',
'organization.settings.name.updated': 'Organization name updated',
'organization.settings.subscription.title': 'Subscription',
'organization.settings.subscription.description': 'Manage your billing, invoices and payment methods.',
'organization.settings.subscription.manage': 'Manage subscription',
'organization.settings.subscription.error': 'Failed to get customer portal URL',
'organization.settings.delete.title': 'Delete organization',
'organization.settings.delete.description': 'Deleting this organization will permanently remove all data associated with it.',
'organization.settings.delete.confirm.title': 'Delete organization',
'organization.settings.delete.confirm.message': 'Are you sure you want to delete this organization? The organization will be marked for deletion and permanently removed after {{ days }} days. During this period, you can restore it from your organizations list. All documents and data will be permanently deleted after this delay.',
'organization.settings.delete.confirm.confirm-button': 'Delete organization',
'organization.settings.delete.confirm.cancel-button': 'Cancel',
'organization.settings.delete.success': 'Organization deleted',
'organization.settings.delete.only-owner': 'Only the organization owner can delete this organization.',
'organization.usage.page.title': 'Usage',
'organization.usage.page.description': 'View your organization\'s current usage and limits.',
'organization.usage.storage.title': 'Document storage',
'organization.usage.storage.description': 'Total storage used by your documents',
'organization.usage.intake-emails.title': 'Intake emails',
'organization.usage.intake-emails.description': 'Number of intake email addresses',
'organization.usage.members.title': 'Members',
'organization.usage.members.description': 'Number of members in the organization',
'organization.usage.unlimited': 'Unlimited',
'organizations.members.title': 'Members',
'organizations.members.description': 'Manage your organization members',
'organizations.members.invite-member': 'Invite member',
'organizations.members.invite-member-disabled-tooltip': 'Only admins or owners can invite members to the organization',
'organizations.members.remove-from-organization': 'Remove from organization',
'organizations.members.role': 'Role',
'organizations.members.roles.owner': 'Owner',
'organizations.members.roles.admin': 'Admin',
'organizations.members.roles.member': 'Member',
'organizations.members.delete.confirm.title': 'Remove member',
'organizations.members.delete.confirm.message': 'Are you sure you want to remove this member from the organization?',
'organizations.members.delete.confirm.confirm-button': 'Remove',
'organizations.members.delete.confirm.cancel-button': 'Cancel',
'organizations.members.delete.success': 'Member removed from organization',
'organizations.members.update-role.success': 'Member role updated',
'organizations.members.table.headers.name': 'Name',
'organizations.members.table.headers.email': 'Email',
'organizations.members.table.headers.role': 'Role',
'organizations.members.table.headers.created': 'Created',
'organizations.members.table.headers.actions': 'Actions',
'organizations.invite-member.title': 'Invite member',
'organizations.invite-member.description': 'Invite a member to your organization',
'organizations.invite-member.form.email.label': 'Email',
'organizations.invite-member.form.email.placeholder': 'Example: ada@papra.app',
'organizations.invite-member.form.email.required': 'Please enter a valid email address',
'organizations.invite-member.form.role.label': 'Role',
'organizations.invite-member.form.submit': 'Invite to organization',
'organizations.invite-member.success.message': 'Member invited',
'organizations.invite-member.success.description': 'The email has been invited to the organization.',
'organizations.invite-member.error.message': 'Failed to invite member',
'organizations.invitations.title': 'Invitations',
'organizations.invitations.description': 'Manage your organization invitations',
'organizations.invitations.list.cta': 'Invite member',
'organizations.invitations.list.empty.title': 'No pending invitations',
'organizations.invitations.list.empty.description': 'You haven\'t been invited to any organizations yet.',
'organizations.invitations.status.pending': 'Pending',
'organizations.invitations.status.accepted': 'Accepted',
'organizations.invitations.status.rejected': 'Rejected',
'organizations.invitations.status.expired': 'Expired',
'organizations.invitations.status.cancelled': 'Cancelled',
'organizations.invitations.resend': 'Resend invitation',
'organizations.invitations.cancel.title': 'Cancel invitation',
'organizations.invitations.cancel.description': 'Are you sure you want to cancel this invitation?',
'organizations.invitations.cancel.confirm': 'Cancel invitation',
'organizations.invitations.cancel.cancel': 'Cancel',
'organizations.invitations.resend.title': 'Resend invitation',
'organizations.invitations.resend.description': 'Are you sure you want to resend this invitation? This will send a new email to the recipient.',
'organizations.invitations.resend.confirm': 'Resend invitation',
'organizations.invitations.resend.cancel': 'Cancel',
'invitations.list.title': 'Invitations',
'invitations.list.description': 'Manage your organization invitations',
'invitations.list.empty.title': 'No pending invitations',
'invitations.list.empty.description': 'You haven\'t been invited to any organizations yet.',
'invitations.list.headers.organization': 'Organization',
'invitations.list.headers.status': 'Status',
'invitations.list.headers.created': 'Created',
'invitations.list.headers.actions': 'Actions',
'invitations.list.actions.accept': 'Accept',
'invitations.list.actions.reject': 'Reject',
'invitations.list.actions.accept.success.message': 'Invitation accepted',
'invitations.list.actions.accept.success.description': 'The invitation has been accepted.',
'invitations.list.actions.reject.success.message': 'Invitation rejected',
'invitations.list.actions.reject.success.description': 'The invitation has been rejected.',
// Documents
'documents.list.title': 'Documents',
'documents.list.no-documents.title': 'No documents',
'documents.list.no-documents.description': 'There are no documents in this organization yet. Start by uploading some documents.',
'documents.list.no-results': 'No documents found',
'documents.tabs.info': 'Info',
'documents.tabs.content': 'Content',
'documents.tabs.activity': 'Activity',
'documents.deleted.message': 'This document has been deleted and will be permanently removed in {{ days }} days.',
'documents.actions.download': 'Download',
'documents.actions.open-in-new-tab': 'Open in new tab',
'documents.actions.restore': 'Restore',
'documents.actions.delete': 'Delete',
'documents.actions.edit': 'Edit',
'documents.actions.cancel': 'Cancel',
'documents.actions.save': 'Save',
'documents.actions.saving': 'Saving...',
'documents.content.alert': 'The content of the document is automatically extracted from the document on upload. It is only used for search and indexing purposes.',
'documents.content.empty-placeholder': 'This document has no extracted content, you can set it manually here.',
'documents.info.id': 'ID',
'documents.info.name': 'Name',
'documents.info.type': 'Type',
'documents.info.size': 'Size',
'documents.info.created-at': 'Created At',
'documents.info.updated-at': 'Updated At',
'documents.info.never': 'Never',
'documents.rename.title': 'Rename document',
'documents.rename.form.name.label': 'Name',
'documents.rename.form.name.placeholder': 'Example: Invoice 2024',
'documents.rename.form.name.required': 'Please enter a name for the document',
'documents.rename.form.name.max-length': 'The name must be less than 255 characters',
'documents.rename.form.submit': 'Rename document',
'documents.rename.success': 'Document renamed successfully',
'documents.rename.cancel': 'Cancel',
'import-documents.title.error': '{{ count }} documents failed',
'import-documents.title.success': '{{ count }} documents imported',
'import-documents.title.pending': '{{ count }} / {{ total }} documents imported',
'import-documents.title.none': 'Import documents',
'import-documents.no-import-in-progress': 'No document import in progress',
'documents.deleted.title': 'Deleted documents',
'documents.deleted.empty.title': 'No deleted documents',
'documents.deleted.empty.description': 'You have no deleted documents. Documents that are deleted will be moved to the trash bin for {{ days }} days.',
'documents.deleted.retention-notice': 'All deleted documents are stored in the trash bin for {{ days }} days. Passing this delay, the documents will be permanently deleted, and you will not be able to restore them.',
'documents.deleted.deleted-at': 'Deleted',
'documents.deleted.restoring': 'Restoring...',
'documents.deleted.deleting': 'Deleting...',
'documents.preview.unknown-file-type': 'No preview available for this file type',
'documents.preview.binary-file': 'This appears to be a binary file and cannot be displayed as text',
'trash.delete-all.button': 'Delete all',
'trash.delete-all.confirm.title': 'Permanently delete all documents?',
'trash.delete-all.confirm.description': 'Are you sure you want to permanently delete all documents from the trash? This action cannot be undone.',
'trash.delete-all.confirm.label': 'Delete',
'trash.delete-all.confirm.cancel': 'Cancel',
'trash.delete.button': 'Delete',
'trash.delete.confirm.title': 'Permanently delete document?',
'trash.delete.confirm.description': 'Are you sure you want to permanently delete this document from the trash? This action cannot be undone.',
'trash.delete.confirm.label': 'Delete',
'trash.delete.confirm.cancel': 'Cancel',
'trash.deleted.success.title': 'Document deleted',
'trash.deleted.success.description': 'The document has been permanently deleted.',
'activity.document.created': 'The document has been created',
'activity.document.updated.single': 'The {{ field }} has been updated',
'activity.document.updated.multiple': 'The {{ fields }} have been updated',
'activity.document.updated': 'The document has been updated',
'activity.document.deleted': 'The document has been deleted',
'activity.document.restored': 'The document has been restored',
'activity.document.tagged': 'Tag {{ tag }} has been added',
'activity.document.untagged': 'Tag {{ tag }} has been removed',
'activity.document.user.name': 'by {{ name }}',
'activity.load-more': 'Load more',
'activity.no-more-activities': 'No more activities for this document',
// Tags
'tags.no-tags.title': 'No tags yet',
'tags.no-tags.description': 'This organization has no tags yet. Tags are used to categorize documents. You can add tags to your documents to make them easier to find and organize.',
'tags.no-tags.create-tag': 'Create tag',
'tags.title': 'Documents Tags',
'tags.description': 'Tags are used to categorize documents. You can add tags to your documents to make them easier to find and organize.',
'tags.create': 'Create tag',
'tags.update': 'Update tag',
'tags.delete': 'Delete tag',
'tags.delete.confirm.title': 'Delete tag',
'tags.delete.confirm.message': 'Are you sure you want to delete this tag? Deleting a tag will remove it from all documents.',
'tags.delete.confirm.confirm-button': 'Delete',
'tags.delete.confirm.cancel-button': 'Cancel',
'tags.delete.success': 'Tag deleted successfully',
'tags.create.success': 'Tag "{{ name }}" created successfully.',
'tags.update.success': 'Tag "{{ name }}" updated successfully.',
'tags.form.name.label': 'Name',
'tags.form.name.placeholder': 'Eg. Contracts',
'tags.form.name.required': 'Please enter a tag name',
'tags.form.name.max-length': 'Tag name must be less than 64 characters',
'tags.form.color.label': 'Color',
'tags.form.color.required': 'Please enter a color',
'tags.form.color.invalid': 'The hex color is badly formatted.',
'tags.form.description.label': 'Description',
'tags.form.description.optional': '(optional)',
'tags.form.description.placeholder': 'Eg. All the contracts signed by the company',
'tags.form.description.max-length': 'Description must be less than 256 characters',
'tags.form.no-description': 'No description',
'tags.table.headers.tag': 'Tag',
'tags.table.headers.description': 'Description',
'tags.table.headers.documents': 'Documents',
'tags.table.headers.created': 'Created',
'tags.table.headers.actions': 'Actions',
// Tagging rules
'tagging-rules.field.name': 'document name',
'tagging-rules.field.content': 'document content',
'tagging-rules.operator.equals': 'equals',
'tagging-rules.operator.not-equals': 'not equals',
'tagging-rules.operator.contains': 'contains',
'tagging-rules.operator.not-contains': 'not contains',
'tagging-rules.operator.starts-with': 'starts with',
'tagging-rules.operator.ends-with': 'ends with',
'tagging-rules.list.title': 'Tagging rules',
'tagging-rules.list.description': 'Manage your organization\'s tagging rules, to automatically tag documents based on conditions you define.',
'tagging-rules.list.demo-warning': 'Note: As this is a demo environment (with no server), tagging rules will not be applied to newly added documents.',
'tagging-rules.list.no-tagging-rules.title': 'No tagging rules',
'tagging-rules.list.no-tagging-rules.description': 'Create a tagging rule to automatically tag your added documents based on conditions you define.',
'tagging-rules.list.no-tagging-rules.create-tagging-rule': 'Create tagging rule',
'tagging-rules.list.card.no-conditions': 'No conditions',
'tagging-rules.list.card.one-condition': '1 condition',
'tagging-rules.list.card.conditions': '{{ count }} conditions',
'tagging-rules.list.card.delete': 'Delete rule',
'tagging-rules.list.card.edit': 'Edit rule',
'tagging-rules.create.title': 'Create tagging rule',
'tagging-rules.create.success': 'Tagging rule created successfully',
'tagging-rules.create.error': 'Failed to create tagging rule',
'tagging-rules.create.submit': 'Create rule',
'tagging-rules.form.name.label': 'Name',
'tagging-rules.form.name.placeholder': 'Example: Tag invoices',
'tagging-rules.form.name.min-length': 'Please enter a name for the rule',
'tagging-rules.form.name.max-length': 'The name must be less than 64 characters',
'tagging-rules.form.description.label': 'Description',
'tagging-rules.form.description.placeholder': 'Example: Tag documents with \'invoice\' in the name',
'tagging-rules.form.description.max-length': 'The description must be less than 256 characters',
'tagging-rules.form.conditions.label': 'Conditions',
'tagging-rules.form.conditions.description': 'Define the conditions that must be met for the rule to apply. All conditions must be met for the rule to apply.',
'tagging-rules.form.conditions.add-condition': 'Add condition',
'tagging-rules.form.conditions.no-conditions.title': 'No conditions',
'tagging-rules.form.conditions.no-conditions.description': 'You didn\'t add any conditions to this rule. This rule will apply its tags to all documents.',
'tagging-rules.form.conditions.no-conditions.confirm': 'Apply rule without conditions',
'tagging-rules.form.conditions.no-conditions.cancel': 'Cancel',
'tagging-rules.form.conditions.value.placeholder': 'Example: invoice',
'tagging-rules.form.conditions.value.min-length': 'Please enter a value for the condition',
'tagging-rules.form.tags.label': 'Tags',
'tagging-rules.form.tags.description': 'Select the tags to apply to the added documents that match the conditions',
'tagging-rules.form.tags.min-length': 'At least one tag to apply is required',
'tagging-rules.form.tags.add-tag': 'Create tag',
'tagging-rules.form.submit': 'Create rule',
'tagging-rules.update.title': 'Update tagging rule',
'tagging-rules.update.error': 'Failed to update tagging rule',
'tagging-rules.update.submit': 'Update rule',
'tagging-rules.update.cancel': 'Cancel',
// Intake emails
'intake-emails.title': 'Intake Emails',
'intake-emails.description': 'Intake emails address are used to automatically ingest emails into Papra. Just forward emails to the intake email address and their attachments will be added to your organization\'s documents.',
'intake-emails.disabled.title': 'Intake Emails are disabled',
'intake-emails.disabled.description': 'Intake emails are disabled on this instance. Please contact your administrator to enable them. See the {{ documentation }} for more information.',
'intake-emails.disabled.documentation': 'documentation',
'intake-emails.info': 'Only enabled intake emails from allowed origins will be processed. You can enable or disable an intake email at any time.',
'intake-emails.empty.title': 'No intake emails',
'intake-emails.empty.description': 'Generate an intake address to easily ingest emails attachments.',
'intake-emails.empty.generate': 'Generate intake email',
'intake-emails.count': '{{ count }} intake email{{ plural }} for this organization',
'intake-emails.new': 'New intake email',
'intake-emails.disabled-label': '(Disabled)',
'intake-emails.no-origins': 'No allowed email origins',
'intake-emails.allowed-origins': 'Allowed from {{ count }} address{{ plural }}',
'intake-emails.actions.enable': 'Enable',
'intake-emails.actions.disable': 'Disable',
'intake-emails.actions.manage-origins': 'Manage origins addresses',
'intake-emails.actions.delete': 'Delete',
'intake-emails.delete.confirm.title': 'Delete intake email?',
'intake-emails.delete.confirm.message': 'Are you sure you want to delete this intake email? This action cannot be undone.',
'intake-emails.delete.confirm.confirm-button': 'Delete intake email',
'intake-emails.delete.confirm.cancel-button': 'Cancel',
'intake-emails.delete.success': 'Intake email deleted',
'intake-emails.create.success': 'Intake email created',
'intake-emails.update.success.enabled': 'Intake email enabled',
'intake-emails.update.success.disabled': 'Intake email disabled',
'intake-emails.allowed-origins.title': 'Allowed origins',
'intake-emails.allowed-origins.description': 'Only emails sent to {{ email }} from these origins will be processed. If no origins are specified, all emails will be discarded.',
'intake-emails.allowed-origins.add.label': 'Add allowed origin email',
'intake-emails.allowed-origins.add.placeholder': 'Eg. ada@papra.app',
'intake-emails.allowed-origins.add.button': 'Add',
'intake-emails.allowed-origins.add.error.exists': 'This email is already in the allowed origins for this intake email',
// API keys
'api-keys.permissions.select-all': 'Select all',
'api-keys.permissions.deselect-all': 'Deselect all',
'api-keys.permissions.organizations.title': 'Organizations',
'api-keys.permissions.organizations.organizations:create': 'Create organizations',
'api-keys.permissions.organizations.organizations:read': 'Read organizations',
'api-keys.permissions.organizations.organizations:update': 'Update organizations',
'api-keys.permissions.organizations.organizations:delete': 'Delete organizations',
'api-keys.permissions.documents.title': 'Documents',
'api-keys.permissions.documents.documents:create': 'Create documents',
'api-keys.permissions.documents.documents:read': 'Read documents',
'api-keys.permissions.documents.documents:update': 'Update documents',
'api-keys.permissions.documents.documents:delete': 'Delete documents',
'api-keys.permissions.tags.title': 'Tags',
'api-keys.permissions.tags.tags:create': 'Create tags',
'api-keys.permissions.tags.tags:read': 'Read tags',
'api-keys.permissions.tags.tags:update': 'Update tags',
'api-keys.permissions.tags.tags:delete': 'Delete tags',
'api-keys.create.title': 'Create API key',
'api-keys.create.description': 'Create a new API key to access the Papra API.',
'api-keys.create.success': 'The API key has been created successfully.',
'api-keys.create.back': 'Back to API keys',
'api-keys.create.form.name.label': 'Name',
'api-keys.create.form.name.placeholder': 'Example: My API key',
'api-keys.create.form.name.required': 'Please enter a name for the API key',
'api-keys.create.form.permissions.label': 'Permissions',
'api-keys.create.form.permissions.required': 'Please select at least one permission',
'api-keys.create.form.submit': 'Create API key',
'api-keys.create.created.title': 'API key created',
'api-keys.create.created.description': 'The API key has been created successfully. Save it in a secure location as it will not be displayed again.',
'api-keys.list.title': 'API keys',
'api-keys.list.description': 'Manage your API keys here.',
'api-keys.list.create': 'Create API key',
'api-keys.list.empty.title': 'No API keys',
'api-keys.list.empty.description': 'Create an API key to access the Papra API.',
'api-keys.list.card.last-used': 'Last used',
'api-keys.list.card.never': 'Never',
'api-keys.list.card.created': 'Created',
'api-keys.delete.success': 'The API key has been deleted successfully',
'api-keys.delete.confirm.title': 'Delete API key',
'api-keys.delete.confirm.message': 'Are you sure you want to delete this API key? This action cannot be undone.',
'api-keys.delete.confirm.confirm-button': 'Delete',
'api-keys.delete.confirm.cancel-button': 'Cancel',
// Webhooks
'webhooks.list.title': 'Webhooks',
'webhooks.list.description': 'Manage your organization webhooks',
'webhooks.list.empty.title': 'No webhooks',
'webhooks.list.empty.description': 'Create your first webhook to start receiving events',
'webhooks.list.create': 'Create webhook',
'webhooks.list.card.last-triggered': 'Last triggered',
'webhooks.list.card.never': 'Never',
'webhooks.list.card.created': 'Created',
'webhooks.create.title': 'Create webhook',
'webhooks.create.description': 'Create a new webhook to receive events',
'webhooks.create.success': 'Webhook created successfully',
'webhooks.create.back': 'Back',
'webhooks.create.form.submit': 'Create webhook',
'webhooks.create.form.name.label': 'Webhook name',
'webhooks.create.form.name.placeholder': 'Enter webhook name',
'webhooks.create.form.name.required': 'Name is required',
'webhooks.create.form.url.label': 'Webhook URL',
'webhooks.create.form.url.placeholder': 'Enter webhook URL',
'webhooks.create.form.url.required': 'URL is required',
'webhooks.create.form.url.invalid': 'URL is invalid',
'webhooks.create.form.secret.label': 'Secret',
'webhooks.create.form.secret.placeholder': 'Enter webhook secret',
'webhooks.create.form.events.label': 'Events',
'webhooks.create.form.events.required': 'At least one event is required',
'webhooks.update.title': 'Edit webhook',
'webhooks.update.description': 'Update your webhook details',
'webhooks.update.success': 'Webhook updated successfully',
'webhooks.update.submit': 'Update webhook',
'webhooks.update.cancel': 'Cancel',
'webhooks.update.form.secret.placeholder': 'Enter new secret',
'webhooks.update.form.secret.placeholder-redacted': '[Redacted secret]',
'webhooks.update.form.rotate-secret.button': 'Rotate secret',
'webhooks.delete.success': 'Webhook deleted successfully',
'webhooks.delete.confirm.title': 'Delete webhook',
'webhooks.delete.confirm.message': 'Are you sure you want to delete this webhook?',
'webhooks.delete.confirm.confirm-button': 'Delete',
'webhooks.delete.confirm.cancel-button': 'Cancel',
'webhooks.events.documents.title': 'Documents events',
'webhooks.events.documents.document:created.description': 'Document created',
'webhooks.events.documents.document:deleted.description': 'Document deleted',
'webhooks.events.documents.document:updated.description': 'Document updated',
'webhooks.events.documents.document:tag:added.description': 'A tag is added to a document',
'webhooks.events.documents.document:tag:removed.description': 'A tag is removed from a document',
// Navigation
'layout.menu.home': 'Home',
'layout.menu.documents': 'Documents',
'layout.menu.tags': 'Tags',
'layout.menu.tagging-rules': 'Tagging rules',
'layout.menu.deleted-documents': 'Deleted documents',
'layout.menu.organization-settings': 'Settings',
'layout.menu.api-keys': 'API keys',
'layout.menu.settings': 'Settings',
'layout.menu.account': 'Account',
'layout.menu.general-settings': 'General settings',
'layout.menu.usage': 'Usage',
'layout.menu.intake-emails': 'Intake emails',
'layout.menu.webhooks': 'Webhooks',
'layout.menu.members': 'Members',
'layout.menu.invitations': 'Invitations',
'layout.upgrade-cta.title': 'Need more space?',
'layout.upgrade-cta.description': 'Get 10x more storage + team collaboration',
'layout.upgrade-cta.button': 'Upgrade now',
'layout.theme.light': 'Light mode',
'layout.theme.dark': 'Dark mode',
'layout.theme.system': 'System mode',
'layout.search.placeholder': 'Search...',
'layout.menu.import-document': 'Import a document',
'user-menu.account-settings': 'Account settings',
'user-menu.api-keys': 'API keys',
'user-menu.invitations': 'Invitations',
'user-menu.language': 'Language',
'user-menu.logout': 'Logout',
// Command palette
'command-palette.search.placeholder': 'Search commands or documents',
'command-palette.no-results': 'No results found',
'command-palette.sections.documents': 'Documents',
'command-palette.sections.theme': 'Theme',
// API errors
'api-errors.document.already_exists': 'The document already exists',
'api-errors.document.size_too_large': 'The file size is too large',
'api-errors.intake-emails.already_exists': 'An intake email with this address already exists.',
'api-errors.intake_email.limit_reached': 'The maximum number of intake emails for this organization has been reached. Please upgrade your plan to create more intake emails.',
'api-errors.user.max_organization_count_reached': 'You have reached the maximum number of organizations you can create, if you need to create more, please contact support.',
'api-errors.default': 'An error occurred while processing your request.',
'api-errors.organization.invitation_already_exists': 'An invitation for this email already exists in this organization.',
'api-errors.user.already_in_organization': 'This user is already in this organization.',
'api-errors.user.organization_invitation_limit_reached': 'The maximum number of invitations has been reached for today. Please try again tomorrow.',
'api-errors.demo.not_available': 'This feature is not available in demo',
'api-errors.tags.already_exists': 'A tag with this name already exists for this organization',
'api-errors.internal.error': 'An error occurred while processing your request. Please try again later.',
'api-errors.auth.invalid_origin': 'Invalid application origin. If you are self-hosting Papra, ensure your APP_BASE_URL environment variable matches your current url. For more details see https://docs.papra.app/resources/troubleshooting/#invalid-application-origin',
'api-errors.organization.max_members_count_reached': 'The maximum number of members and pending invitations for this organization has been reached. Please upgrade your plan to add more members.',
// Not found
'not-found.title': '404 - Not Found',
'not-found.description': 'Sorry, the page you are looking for does not seem to exist. Please check the URL and try again.',
'not-found.back-to-home': 'Go back to home',
// Demo
'demo.popup.description': 'This is a demo environment, all data is save to your browser local storage.',
'demo.popup.discord': 'Join the {{ discordLink }} to get support, propose features or just chat.',
'demo.popup.discord-link-label': 'Discord server',
'demo.popup.reset': 'Reset demo data',
'demo.popup.hide': 'Hide',
// Color picker
'color-picker.hue': 'Hue',
'color-picker.saturation': 'Saturation',
'color-picker.lightness': 'Lightness',
'color-picker.select-color': 'Select color',
'color-picker.select-a-color': 'Select a color',
// Subscriptions
'subscriptions.checkout-success.title': 'Payment Successful!',
'subscriptions.checkout-success.description': 'Your subscription has been activated successfully.',
'subscriptions.checkout-success.thank-you': 'Thank you for upgrading to Papra Plus. You now have access to all premium features.',
'subscriptions.checkout-success.go-to-organizations': 'Go to Organizations',
'subscriptions.checkout-success.redirecting': 'Redirecting in {{ count }} second{{ plural }}...',
'subscriptions.checkout-cancel.title': 'Payment Canceled',
'subscriptions.checkout-cancel.description': 'Your subscription upgrade was canceled.',
'subscriptions.checkout-cancel.no-charges': 'No charges have been made to your account. You can try again anytime you\'re ready.',
'subscriptions.checkout-cancel.back-to-organizations': 'Back to Organizations',
'subscriptions.checkout-cancel.need-help': 'Need help?',
'subscriptions.checkout-cancel.contact-support': 'Contact support',
'subscriptions.upgrade-dialog.title': 'Upgrade this organization',
'subscriptions.upgrade-dialog.description': 'Unlock powerful features for your organization',
'subscriptions.upgrade-dialog.contact-us': 'Contact us',
'subscriptions.upgrade-dialog.enterprise-plans': 'if you need custom enterprise plans.',
'subscriptions.upgrade-dialog.current-plan': 'Current Plan',
'subscriptions.upgrade-dialog.recommended': 'Recommended',
'subscriptions.upgrade-dialog.per-month': '/month',
'subscriptions.upgrade-dialog.billed-annually': '${{ price }} billed annually',
'subscriptions.upgrade-dialog.upgrade-now': 'Upgrade now',
'subscriptions.plan.free.name': 'Free plan',
'subscriptions.plan.plus.name': 'Plus',
'subscriptions.plan.pro.name': 'Pro',
'subscriptions.features.storage-size': 'Document storage size',
'subscriptions.features.members': 'Organization Members',
'subscriptions.features.members-count': '{{ count }} members',
'subscriptions.features.email-intakes': 'Email Intakes',
'subscriptions.features.email-intakes-count-singular': '{{ count }} address',
'subscriptions.features.email-intakes-count-plural': '{{ count }} addresses',
'subscriptions.features.max-upload-size': 'Max upload file size',
'subscriptions.features.support': 'Support',
'subscriptions.features.support-community': 'Community support',
'subscriptions.features.support-email': 'Email support',
'subscriptions.features.support-priority': 'Priority support',
'subscriptions.billing-interval.monthly': 'Monthly',
'subscriptions.billing-interval.annual': 'Annual',
'subscriptions.usage-warning.message': 'You have used {{ percent }}% of your document storage. Consider upgrading your plan to get more space.',
'subscriptions.usage-warning.upgrade-button': 'Upgrade Plan',
// Common / Shared
'common.confirm-modal.type-to-confirm': 'Type "{{ text }}" to confirm',
} as const;

View File

@@ -1,245 +0,0 @@
auth.request-password-reset.title: Reset your password
auth.request-password-reset.description: Enter your email to reset your password.
auth.request-password-reset.requested: If an account exists for this email, we've sent you an email to reset your password.
auth.request-password-reset.back-to-login: Back to login
auth.request-password-reset.form.email.label: Email
auth.request-password-reset.form.email.placeholder: 'Example: ada@papra.app'
auth.request-password-reset.form.email.required: Please enter your email address
auth.request-password-reset.form.email.invalid: This email address is invalid
auth.request-password-reset.form.submit: Request password reset
auth.reset-password.title: Reset your password
auth.reset-password.description: Enter your new password to reset your password.
auth.reset-password.reset: Your password has been reset.
auth.reset-password.back-to-login: Back to login
auth.reset-password.form.new-password.label: New password
auth.reset-password.form.new-password.placeholder: 'Example: **********'
auth.reset-password.form.new-password.required: Please enter your new password
auth.reset-password.form.new-password.min-length: Password must be at least {{ minLength }} characters
auth.reset-password.form.new-password.max-length: Password must be less than {{ maxLength }} characters
auth.reset-password.form.submit: Reset password
auth.email-provider.open: Open {{ provider }}
auth.login.title: Login to Papra
auth.login.description: Enter your email or use social login to access your Papra account.
auth.login.login-with-provider: Login with {{ provider }}
auth.login.no-account: Don't have an account?
auth.login.register: Register
auth.login.form.email.label: Email
auth.login.form.email.placeholder: 'Example: ada@papra.app'
auth.login.form.email.required: Please enter your email address
auth.login.form.email.invalid: This email address is invalid
auth.login.form.password.label: Password
auth.login.form.password.placeholder: Set a password
auth.login.form.password.required: Please enter your password
auth.login.form.remember-me.label: Remember me
auth.login.form.forgot-password.label: Forgot password?
auth.login.form.submit: Login
auth.register.title: Register to Papra
auth.register.description: Enter your email or use social login to access your Papra account.
auth.register.register-with-email: Register with email
auth.register.register-with-provider: Register with {{ provider }}
auth.register.providers.google: Google
auth.register.providers.github: GitHub
auth.register.have-account: Already have an account?
auth.register.login: Login
auth.register.registration-disabled.title: Registration is disabled
auth.register.registration-disabled.description: The creation of new accounts is currently disabled on this instance of Papra. Only users with existing accounts can log in. If you think this is a mistake, please contact the administrator of this instance.
auth.register.form.email.label: Email
auth.register.form.email.placeholder: 'Example: ada@papra.app'
auth.register.form.email.required: Please enter your email address
auth.register.form.email.invalid: This email address is invalid
auth.register.form.password.label: Password
auth.register.form.password.placeholder: Set a password
auth.register.form.password.required: Please enter your password
auth.register.form.password.min-length: Password must be at least {{ minLength }} characters
auth.register.form.password.max-length: Password must be less than {{ maxLength }} characters
auth.register.form.name.label: Name
auth.register.form.name.placeholder: 'Example: Ada Lovelace'
auth.register.form.name.required: Please enter your name
auth.register.form.name.max-length: Name must be less than {{ maxLength }} characters
auth.register.form.submit: Register
auth.email-validation-required.title: Verify your email
auth.email-validation-required.description: A verification email has been sent to your email address. Please verify your email address by clicking the link in the email.
auth.legal-links.description: By continuing, you acknowledge that you understand and agree to the {{ terms }} and {{ privacy }}.
auth.legal-links.terms: Terms of Service
auth.legal-links.privacy: Privacy Policy
tags.no-tags.title: No tags yet
tags.no-tags.description: This organization has no tags yet. Tags are used to categorize documents. You can add tags to your documents to make them easier to find and organize.
tags.no-tags.create-tag: Create tag
layout.menu.home: Home
layout.menu.documents: Documents
layout.menu.tags: Tags
layout.menu.tagging-rules: Tagging rules
layout.menu.deleted-documents: Deleted documents
layout.menu.organization-settings: Settings
layout.menu.api-keys: API keys
layout.menu.settings: Settings
layout.menu.account: Account
layout.menu.general-settings: General settings
layout.menu.intake-emails: Intake emails
layout.menu.webhooks: Webhooks
tagging-rules.field.name: document name
tagging-rules.field.content: document content
tagging-rules.operator.equals: equals
tagging-rules.operator.not-equals: not equals
tagging-rules.operator.contains: contains
tagging-rules.operator.not-contains: not contains
tagging-rules.operator.starts-with: starts with
tagging-rules.operator.ends-with: ends with
tagging-rules.list.title: Tagging rules
tagging-rules.list.description: Manage your organization's tagging rules, to automatically tag documents based on conditions you define.
tagging-rules.list.demo-warning: 'Note: As this is a demo environment (with no server), tagging rules will not be applied to newly added documents.'
tagging-rules.list.no-tagging-rules.title: No tagging rules
tagging-rules.list.no-tagging-rules.description: Create a tagging rule to automatically tag your added documents based on conditions you define.
tagging-rules.list.no-tagging-rules.create-tagging-rule: Create tagging rule
tagging-rules.list.card.no-conditions: No conditions
tagging-rules.list.card.one-condition: 1 condition
tagging-rules.list.card.conditions: '{{ count }} conditions'
tagging-rules.list.card.delete: Delete rule
tagging-rules.list.card.edit: Edit rule
tagging-rules.create.title: Create tagging rule
tagging-rules.create.success: Tagging rule created successfully
tagging-rules.create.error: Failed to create tagging rule
tagging-rules.create.submit: Create rule
tagging-rules.form.name.label: Name
tagging-rules.form.name.placeholder: 'Example: Tag invoices'
tagging-rules.form.name.min-length: Please enter a name for the rule
tagging-rules.form.name.max-length: The name must be less than 64 characters
tagging-rules.form.description.label: Description
tagging-rules.form.description.placeholder: "Example: Tag documents with 'invoice' in the name"
tagging-rules.form.description.max-length: The description must be less than 256 characters
tagging-rules.form.conditions.label: Conditions
tagging-rules.form.conditions.description: Define the conditions that must be met for the rule to apply. All conditions must be met for the rule to apply.
tagging-rules.form.conditions.add-condition: Add condition
tagging-rules.form.conditions.no-conditions.title: No conditions
tagging-rules.form.conditions.no-conditions.description: You didn't add any conditions to this rule. This rule will apply its tags to all documents.
tagging-rules.form.conditions.no-conditions.confirm: Apply rule without conditions
tagging-rules.form.conditions.no-conditions.cancel: Cancel
tagging-rules.form.conditions.value.placeholder: 'Example: invoice'
tagging-rules.form.conditions.value.min-length: Please enter a value for the condition
tagging-rules.form.tags.label: Tags
tagging-rules.form.tags.description: Select the tags to apply to the added documents that match the conditions
tagging-rules.form.tags.min-length: At least one tag to apply is required
tagging-rules.form.tags.add-tag: Create tag
tagging-rules.form.submit: Create rule
tagging-rules.update.title: Update tagging rule
tagging-rules.update.error: Failed to update tagging rule
tagging-rules.update.submit: Update rule
tagging-rules.update.cancel: Cancel
demo.popup.description: This is a demo environment, all data is save to your browser local storage.
demo.popup.discord: Join the {{ discordLink }} to get support, propose features or just chat.
demo.popup.discord-link-label: Discord server
demo.popup.reset: Reset demo data
demo.popup.hide: Hide
trash.delete-all.button: Delete all
trash.delete-all.confirm.title: Permanently delete all documents?
trash.delete-all.confirm.description: Are you sure you want to permanently delete all documents from the trash? This action cannot be undone.
trash.delete-all.confirm.label: Delete
trash.delete-all.confirm.cancel: Cancel
trash.delete.button: Delete
trash.delete.confirm.title: Permanently delete document?
trash.delete.confirm.description: Are you sure you want to permanently delete this document from the trash? This action cannot be undone.
trash.delete.confirm.label: Delete
trash.delete.confirm.cancel: Cancel
trash.deleted.success.title: Document deleted
trash.deleted.success.description: The document has been permanently deleted.
import-documents.title.error: '{{ count }} documents failed'
import-documents.title.success: '{{ count }} documents imported'
import-documents.title.pending: '{{ count }} / {{ total }} documents imported'
import-documents.title.none: Import documents
import-documents.no-import-in-progress: No document import in progress
api-errors.document.already_exists: The document already exists
api-errors.document.file_too_big: The document file is too big
api-errors.intake_email.limit_reached: The maximum number of intake emails for this organization has been reached. Please upgrade your plan to create more intake emails.
api-errors.user.max_organization_count_reached: You have reached the maximum number of organizations you can create, if you need to create more, please contact support.
api-errors.default: An error occurred while processing your request.
api-keys.permissions.documents.title: Documents
api-keys.permissions.documents.documents:create: Create documents
api-keys.permissions.documents.documents:read: Read documents
api-keys.permissions.documents.documents:update: Update documents
api-keys.permissions.documents.documents:delete: Delete documents
api-keys.permissions.tags.title: Tags
api-keys.permissions.tags.tags:create: Create tags
api-keys.permissions.tags.tags:read: Read tags
api-keys.permissions.tags.tags:update: Update tags
api-keys.permissions.tags.tags:delete: Delete tags
api-keys.create.title: Create API key
api-keys.create.description: Create a new API key to access the Papra API.
api-keys.create.success: The API key has been created successfully.
api-keys.create.back: Back to API keys
api-keys.create.form.name.label: Name
api-keys.create.form.name.placeholder: 'Example: My API key'
api-keys.create.form.name.required: Please enter a name for the API key
api-keys.create.form.permissions.label: Permissions
api-keys.create.form.permissions.required: Please select at least one permission
api-keys.create.form.submit: Create API key
api-keys.create.created.title: API key created
api-keys.create.created.description: The API key has been created successfully. Save it in a secure location as it will not be displayed again.
api-keys.list.title: API keys
api-keys.list.description: Manage your API keys here.
api-keys.list.create: Create API key
api-keys.list.empty.title: No API keys
api-keys.list.empty.description: Create an API key to access the Papra API.
api-keys.list.card.last-used: Last used
api-keys.list.card.never: Never
api-keys.list.card.created: Created
api-keys.delete.success: The API key has been deleted successfully
api-keys.delete.confirm.title: Delete API key
api-keys.delete.confirm.message: Are you sure you want to delete this API key? This action cannot be undone.
api-keys.delete.confirm.confirm-button: Delete
api-keys.delete.confirm.cancel-button: Cancel
webhooks.list.title: Webhooks
webhooks.list.description: Manage your organization webhooks
webhooks.list.empty.title: No webhooks
webhooks.list.empty.description: Create your first webhook to start receiving events
webhooks.list.create: Create webhook
webhooks.list.card.last-triggered: Last triggered
webhooks.list.card.never: Never
webhooks.list.card.created: Created
webhooks.create.title: Create webhook
webhooks.create.description: Create a new webhook to receive events
webhooks.create.success: Webhook created successfully
webhooks.create.back: Back
webhooks.create.form.submit: Create webhook
webhooks.create.form.name.label: Webhook name
webhooks.create.form.name.placeholder: Enter webhook name
webhooks.create.form.name.required: Name is required
webhooks.create.form.url.label: Webhook URL
webhooks.create.form.url.placeholder: Enter webhook URL
webhooks.create.form.url.required: URL is required
webhooks.create.form.url.invalid: URL is invalid
webhooks.create.form.secret.label: Secret
webhooks.create.form.secret.placeholder: Enter webhook secret
webhooks.create.form.events.label: Events
webhooks.create.form.events.required: At least one event is required
webhooks.update.title: Edit webhook
webhooks.update.description: Update your webhook details
webhooks.update.success: Webhook updated successfully
webhooks.update.submit: Update webhook
webhooks.update.cancel: Cancel
webhooks.update.form.secret.placeholder: Enter new secret
webhooks.update.form.secret.placeholder-redacted: '[Redacted secret]'
webhooks.update.form.rotate-secret.button: Rotate secret
webhooks.delete.success: Webhook deleted successfully
webhooks.delete.confirm.title: Delete webhook
webhooks.delete.confirm.message: Are you sure you want to delete this webhook?
webhooks.delete.confirm.confirm-button: Delete
webhooks.delete.confirm.cancel-button: Cancel
webhooks.events.documents.document:created.description: Document created
webhooks.events.documents.document:deleted.description: Document deleted

View File

@@ -0,0 +1,665 @@
import type { TranslationsDictionary } from '@/modules/i18n/locales.types';
export const translations: Partial<TranslationsDictionary> = {
// Authentication
'auth.request-password-reset.title': 'Restablece tu contraseña',
'auth.request-password-reset.description': 'Ingresa tu correo electrónico para restablecer tu contraseña.',
'auth.request-password-reset.requested': 'Si existe una cuenta para este correo electrónico, te enviaremos un correo para restablecer tu contraseña.',
'auth.request-password-reset.back-to-login': 'Volver al inicio de sesión',
'auth.request-password-reset.form.email.label': 'Correo electrónico',
'auth.request-password-reset.form.email.placeholder': 'Ejemplo: ada@papra.app',
'auth.request-password-reset.form.email.required': 'Por favor, ingresa tu correo electrónico',
'auth.request-password-reset.form.email.invalid': 'Esta dirección de correo electrónico no es válida',
'auth.request-password-reset.form.submit': 'Solicitar restablecimiento de contraseña',
'auth.reset-password.title': 'Restablece tu contraseña',
'auth.reset-password.description': 'Ingresa tu nueva contraseña para restablecerla.',
'auth.reset-password.reset': 'Tu contraseña ha sido restablecida.',
'auth.reset-password.back-to-login': 'Volver al inicio de sesión',
'auth.reset-password.form.new-password.label': 'Nueva contraseña',
'auth.reset-password.form.new-password.placeholder': 'Ejemplo: **********',
'auth.reset-password.form.new-password.required': 'Por favor, ingresa tu nueva contraseña',
'auth.reset-password.form.new-password.min-length': 'La contraseña debe tener al menos {{ minLength }} caracteres',
'auth.reset-password.form.new-password.max-length': 'La contraseña debe tener menos de {{ maxLength }} caracteres',
'auth.reset-password.form.submit': 'Restablecer contraseña',
'auth.email-provider.open': 'Abrir {{ provider }}',
'auth.login.title': 'Inicia sesión en Papra',
'auth.login.description': 'Ingresa tu correo electrónico o usa un inicio de sesión social para acceder a tu cuenta de Papra.',
'auth.login.login-with-provider': 'Iniciar sesión con {{ provider }}',
'auth.login.no-account': '¿No tienes una cuenta?',
'auth.login.register': 'Registrarse',
'auth.login.form.email.label': 'Correo electrónico',
'auth.login.form.email.placeholder': 'Ejemplo: ada@papra.app',
'auth.login.form.email.required': 'Por favor, ingresa tu correo electrónico',
'auth.login.form.email.invalid': 'Esta dirección de correo electrónico no es válida',
'auth.login.form.password.label': 'Contraseña',
'auth.login.form.password.placeholder': 'Establece una contraseña',
'auth.login.form.password.required': 'Por favor, ingresa tu contraseña',
'auth.login.form.remember-me.label': 'Recordarme',
'auth.login.form.forgot-password.label': '¿Olvidaste tu contraseña?',
'auth.login.form.submit': 'Iniciar sesión',
'auth.register.title': 'Regístrate en Papra',
'auth.register.description': 'Crea una cuenta para comenzar a usar Papra.',
'auth.register.register-with-email': 'Registrarse con correo electrónico',
'auth.register.register-with-provider': 'Registrarse con {{ provider }}',
'auth.register.providers.google': 'Google',
'auth.register.providers.github': 'GitHub',
'auth.register.have-account': '¿Ya tienes una cuenta?',
'auth.register.login': 'Iniciar sesión',
'auth.register.registration-disabled.title': 'El registro está deshabilitado',
'auth.register.registration-disabled.description': 'La creación de nuevas cuentas está deshabilitada actualmente en esta instancia de Papra. Solo los usuarios con cuentas existentes pueden iniciar sesión. Si crees que esto es un error, contacta al administrador de esta instancia.',
'auth.register.form.email.label': 'Correo electrónico',
'auth.register.form.email.placeholder': 'Ejemplo: ada@papra.app',
'auth.register.form.email.required': 'Por favor, ingresa tu correo electrónico',
'auth.register.form.email.invalid': 'Esta dirección de correo electrónico no es válida',
'auth.register.form.password.label': 'Contraseña',
'auth.register.form.password.placeholder': 'Establece una contraseña',
'auth.register.form.password.required': 'Por favor, ingresa tu contraseña',
'auth.register.form.password.min-length': 'La contraseña debe tener al menos {{ minLength }} caracteres',
'auth.register.form.password.max-length': 'La contraseña debe tener menos de {{ maxLength }} caracteres',
'auth.register.form.name.label': 'Nombre',
'auth.register.form.name.placeholder': 'Ejemplo: Ada Lovelace',
'auth.register.form.name.required': 'Por favor, ingresa tu nombre',
'auth.register.form.name.max-length': 'El nombre debe tener menos de {{ maxLength }} caracteres',
'auth.register.form.submit': 'Registrarse',
'auth.email-validation-required.title': 'Verifica tu correo electrónico',
'auth.email-validation-required.description': 'Se ha enviado un correo de verificación a tu dirección de correo electrónico. Por favor, verifica tu correo haciendo clic en el enlace del correo.',
'auth.legal-links.description': 'Al continuar, reconoces que entiendes y aceptas los {{ terms }} y la {{ privacy }}.',
'auth.legal-links.terms': 'Términos de servicio',
'auth.legal-links.privacy': 'Política de privacidad',
'auth.no-auth-provider.title': 'No hay proveedor de autenticación',
'auth.no-auth-provider.description': 'No hay proveedores de autenticación habilitados en esta instancia de Papra. Por favor, contacta al administrador de esta instancia para habilitarlos.',
// User settings
'user.settings.title': 'Configuración de usuario',
'user.settings.description': 'Administra aquí la configuración de tu cuenta.',
'user.settings.email.title': 'Dirección de correo electrónico',
'user.settings.email.description': 'Tu dirección de correo electrónico no puede ser cambiada.',
'user.settings.email.label': 'Correo electrónico',
'user.settings.name.title': 'Nombre completo',
'user.settings.name.description': 'Tu nombre completo se muestra a otros miembros de la organización.',
'user.settings.name.label': 'Nombre completo',
'user.settings.name.placeholder': 'Ej. John Doe',
'user.settings.name.update': 'Actualizar nombre',
'user.settings.name.updated': 'Tu nombre completo ha sido actualizado',
'user.settings.logout.title': 'Cerrar sesión',
'user.settings.logout.description': 'Cierra la sesión de tu cuenta. Puedes iniciar sesión nuevamente más tarde.',
'user.settings.logout.button': 'Cerrar sesión',
// Organizations
'organizations.list.title': 'Tus organizaciones',
'organizations.list.description': 'Las organizaciones son una manera de agrupar tus documentos y gestionar el acceso a ellos. Puedes crear varias organizaciones e invitar a tus compañeros para colaborar.',
'organizations.list.create-new': 'Crear nueva organización',
'organizations.list.back': 'Volver a organizaciones',
'organizations.list.deleted.title': 'Organizaciones eliminadas',
'organizations.list.deleted.description': 'Las organizaciones eliminadas se conservan durante {{ days }} días antes de ser eliminadas permanentemente. Puedes restaurarlas durante este período.',
'organizations.list.deleted.empty': 'No hay organizaciones eliminadas',
'organizations.list.deleted.empty-description': 'Cuando elimines una organización, aparecerá aquí durante {{ days }} días antes de ser eliminada permanentemente.',
'organizations.list.deleted.restore': 'Restaurar',
'organizations.list.deleted.restore-success': 'Organización restaurada exitosamente',
'organizations.list.deleted.restore-confirm.title': 'Restaurar organización',
'organizations.list.deleted.restore-confirm.message': '¿Estás seguro de que quieres restaurar esta organización? Se moverá de vuelta a tu lista de organizaciones activas.',
'organizations.list.deleted.restore-confirm.confirm-button': 'Restaurar organización',
'organizations.list.deleted.deleted-at': 'Eliminada el {{ date }}',
'organizations.list.deleted.purge-at': 'Se eliminará permanentemente el {{ date }}',
'organizations.list.deleted.days-remaining': '({{ daysUntilPurge, =1:{daysUntilPurge} día, {daysUntilPurge} días }} restante{{ daysUntilPurge, >1:s}})',
'organizations.details.no-documents.title': 'Sin documentos',
'organizations.details.no-documents.description': 'Aún no hay documentos en esta organización. Comienza subiendo algunos documentos.',
'organizations.details.upload-documents': 'Subir documentos',
'organizations.details.documents-count': 'documentos en total',
'organizations.details.total-size': 'tamaño total',
'organizations.details.latest-documents': 'Últimos documentos importados',
'organizations.create.title': 'Crear una nueva organización',
'organizations.create.description': 'Tus documentos se agruparán por organización. Puedes crear varias organizaciones para separar tus documentos, por ejemplo, para documentos personales y de trabajo.',
'organizations.create.back': 'Volver',
'organizations.create.error.max-count-reached': 'Has alcanzado el número máximo de organizaciones que puedes crear, si necesitas crear más, contacta al soporte.',
'organizations.create.form.name.label': 'Nombre de la organización',
'organizations.create.form.name.placeholder': 'Ej. Acme Inc.',
'organizations.create.form.name.required': 'Por favor, ingresa un nombre para la organización',
'organizations.create.form.submit': 'Crear organización',
'organizations.create.success': 'Organización creada exitosamente',
'organizations.create-first.title': 'Crea tu organización',
'organizations.create-first.description': 'Tus documentos se agruparán por organización. Puedes crear varias organizaciones para separar tus documentos, por ejemplo, para documentos personales y de trabajo.',
'organizations.create-first.default-name': 'Mi organización',
'organizations.create-first.user-name': 'Organización de {{ name }}',
'organization.settings.title': 'Configuración de la organización',
'organization.settings.page.title': 'Configuración de la organización',
'organization.settings.page.description': 'Administra la configuración de tu organización aquí.',
'organization.settings.name.title': 'Nombre de la organización',
'organization.settings.name.update': 'Actualizar nombre',
'organization.settings.name.placeholder': 'Ej. Acme Inc.',
'organization.settings.name.updated': 'Nombre de la organización actualizado',
'organization.settings.subscription.title': 'Suscripción',
'organization.settings.subscription.description': 'Administra tu facturación, facturas y métodos de pago.',
'organization.settings.subscription.manage': 'Gestionar suscripción',
'organization.settings.subscription.error': 'Error al obtener la URL del portal del cliente',
'organization.settings.delete.title': 'Eliminar organización',
'organization.settings.delete.description': 'Eliminar esta organización eliminará permanentemente todos los datos asociados a ella.',
'organization.settings.delete.confirm.title': 'Eliminar organización',
'organization.settings.delete.confirm.message': '¿Estás seguro de que deseas eliminar esta organización? La organización se marcará para eliminación y se eliminará permanentemente después de {{ days }} días. Durante este período, puedes restaurarla desde tu lista de organizaciones. Todos los documentos y datos se eliminarán permanentemente después de este plazo.',
'organization.settings.delete.confirm.confirm-button': 'Eliminar organización',
'organization.settings.delete.confirm.cancel-button': 'Cancelar',
'organization.settings.delete.success': 'Organización eliminada',
'organization.settings.delete.only-owner': 'Solo el propietario de la organización puede eliminar esta organización.',
'organization.usage.page.title': 'Uso',
'organization.usage.page.description': 'Ver el uso y los límites actuales de su organización.',
'organization.usage.storage.title': 'Almacenamiento de documentos',
'organization.usage.storage.description': 'Almacenamiento total usado por sus documentos',
'organization.usage.intake-emails.title': 'Correos de ingesta',
'organization.usage.intake-emails.description': 'Número de direcciones de correo de ingesta',
'organization.usage.members.title': 'Miembros',
'organization.usage.members.description': 'Número de miembros en la organización',
'organization.usage.unlimited': 'Ilimitado',
'organizations.members.title': 'Miembros',
'organizations.members.description': 'Administra los miembros de tu organización',
'organizations.members.invite-member': 'Invitar miembro',
'organizations.members.invite-member-disabled-tooltip': 'Solo los administradores o propietarios pueden invitar miembros a la organización',
'organizations.members.remove-from-organization': 'Eliminar de la organización',
'organizations.members.role': 'Rol',
'organizations.members.roles.owner': 'Propietario',
'organizations.members.roles.admin': 'Administrador',
'organizations.members.roles.member': 'Miembro',
'organizations.members.delete.confirm.title': 'Eliminar miembro',
'organizations.members.delete.confirm.message': '¿Estás seguro de que deseas eliminar a este miembro de la organización?',
'organizations.members.delete.confirm.confirm-button': 'Eliminar',
'organizations.members.delete.confirm.cancel-button': 'Cancelar',
'organizations.members.delete.success': 'Miembro eliminado de la organización',
'organizations.members.update-role.success': 'Rol del miembro actualizado',
'organizations.members.table.headers.name': 'Nombre',
'organizations.members.table.headers.email': 'Correo electrónico',
'organizations.members.table.headers.role': 'Rol',
'organizations.members.table.headers.created': 'Creado',
'organizations.members.table.headers.actions': 'Acciones',
'organizations.invite-member.title': 'Invitar miembro',
'organizations.invite-member.description': 'Invita a un miembro a tu organización',
'organizations.invite-member.form.email.label': 'Correo electrónico',
'organizations.invite-member.form.email.placeholder': 'Ejemplo: ada@papra.app',
'organizations.invite-member.form.email.required': 'Por favor, ingresa un correo electrónico válido',
'organizations.invite-member.form.role.label': 'Rol',
'organizations.invite-member.form.submit': 'Invitar a la organización',
'organizations.invite-member.success.message': 'Miembro invitado',
'organizations.invite-member.success.description': 'El correo ha sido invitado a la organización.',
'organizations.invite-member.error.message': 'Error al invitar al miembro',
'organizations.invitations.title': 'Invitaciones',
'organizations.invitations.description': 'Administra las invitaciones de tu organización',
'organizations.invitations.list.cta': 'Invitar miembro',
'organizations.invitations.list.empty.title': 'No hay invitaciones pendientes',
'organizations.invitations.list.empty.description': 'Aún no te han invitado a ninguna organización.',
'organizations.invitations.status.pending': 'Pendiente',
'organizations.invitations.status.accepted': 'Aceptada',
'organizations.invitations.status.rejected': 'Rechazada',
'organizations.invitations.status.expired': 'Expirada',
'organizations.invitations.status.cancelled': 'Cancelada',
'organizations.invitations.resend': 'Reenviar invitación',
'organizations.invitations.cancel.title': 'Cancelar invitación',
'organizations.invitations.cancel.description': '¿Estás seguro de que deseas cancelar esta invitación?',
'organizations.invitations.cancel.confirm': 'Cancelar invitación',
'organizations.invitations.cancel.cancel': 'Cancelar',
'organizations.invitations.resend.title': 'Reenviar invitación',
'organizations.invitations.resend.description': '¿Estás seguro de que deseas reenviar esta invitación? Esto enviará un nuevo correo al destinatario.',
'organizations.invitations.resend.confirm': 'Reenviar invitación',
'organizations.invitations.resend.cancel': 'Cancelar',
'invitations.list.title': 'Invitaciones',
'invitations.list.description': 'Administra las invitaciones de tu organización',
'invitations.list.empty.title': 'No hay invitaciones pendientes',
'invitations.list.empty.description': 'Aún no te han invitado a ninguna organización.',
'invitations.list.headers.organization': 'Organización',
'invitations.list.headers.status': 'Estado',
'invitations.list.headers.created': 'Creado',
'invitations.list.headers.actions': 'Acciones',
'invitations.list.actions.accept': 'Aceptar',
'invitations.list.actions.reject': 'Rechazar',
'invitations.list.actions.accept.success.message': 'Invitación aceptada',
'invitations.list.actions.accept.success.description': 'La invitación ha sido aceptada.',
'invitations.list.actions.reject.success.message': 'Invitación rechazada',
'invitations.list.actions.reject.success.description': 'La invitación ha sido rechazada.',
// Documents
'documents.list.title': 'Documentos',
'documents.list.no-documents.title': 'Sin documentos',
'documents.list.no-documents.description': 'Aún no hay documentos en esta organización. Comienza subiendo algunos documentos.',
'documents.list.no-results': 'No se encontraron documentos',
'documents.tabs.info': 'Información',
'documents.tabs.content': 'Contenido',
'documents.tabs.activity': 'Actividad',
'documents.deleted.message': 'Este documento ha sido eliminado y será borrado permanentemente en {{ days }} días.',
'documents.actions.download': 'Descargar',
'documents.actions.open-in-new-tab': 'Abrir en una nueva pestaña',
'documents.actions.restore': 'Restaurar',
'documents.actions.delete': 'Eliminar',
'documents.actions.edit': 'Editar',
'documents.actions.cancel': 'Cancelar',
'documents.actions.save': 'Guardar',
'documents.actions.saving': 'Guardando...',
'documents.content.alert': 'El contenido del documento se extrae automáticamente al subirlo. Solo se utiliza para búsqueda e indexación.',
'documents.content.empty-placeholder': 'Este documento no tiene contenido extraído, puedes introducirlo manualmente aquí.',
'documents.info.id': 'ID',
'documents.info.name': 'Nombre',
'documents.info.type': 'Tipo',
'documents.info.size': 'Tamaño',
'documents.info.created-at': 'Creado el',
'documents.info.updated-at': 'Actualizado el',
'documents.info.never': 'Nunca',
'documents.rename.title': 'Renombrar documento',
'documents.rename.form.name.label': 'Nombre',
'documents.rename.form.name.placeholder': 'Ejemplo: Factura 2024',
'documents.rename.form.name.required': 'Por favor, ingresa un nombre para el documento',
'documents.rename.form.name.max-length': 'El nombre debe tener menos de 255 caracteres',
'documents.rename.form.submit': 'Renombrar documento',
'documents.rename.success': 'Documento renombrado exitosamente',
'documents.rename.cancel': 'Cancelar',
'import-documents.title.error': '{{ count }} documentos fallidos',
'import-documents.title.success': '{{ count }} documentos importados',
'import-documents.title.pending': '{{ count }} / {{ total }} documentos importados',
'import-documents.title.none': 'Importar documentos',
'import-documents.no-import-in-progress': 'No hay importación de documentos en curso',
'documents.deleted.title': 'Documentos eliminados',
'documents.deleted.empty.title': 'No hay documentos eliminados',
'documents.deleted.empty.description': 'No tienes documentos eliminados. Los documentos eliminados se moverán a la papelera durante {{ days }} días.',
'documents.deleted.retention-notice': 'Todos los documentos eliminados se almacenan en la papelera durante {{ days }} días. Pasado este tiempo, los documentos serán eliminados permanentemente y no podrás restaurarlos.',
'documents.deleted.deleted-at': 'Eliminado',
'documents.deleted.restoring': 'Restaurando...',
'documents.deleted.deleting': 'Eliminando...',
'documents.preview.unknown-file-type': 'No hay vista previa disponible para este tipo de archivo',
'documents.preview.binary-file': 'Este parece ser un archivo binario y no puede mostrarse como texto',
'trash.delete-all.button': 'Eliminar todo',
'trash.delete-all.confirm.title': '¿Eliminar permanentemente todos los documentos?',
'trash.delete-all.confirm.description': '¿Estás seguro de que deseas eliminar permanentemente todos los documentos de la papelera? Esta acción no se puede deshacer.',
'trash.delete-all.confirm.label': 'Eliminar',
'trash.delete-all.confirm.cancel': 'Cancelar',
'trash.delete.button': 'Eliminar',
'trash.delete.confirm.title': '¿Eliminar permanentemente el documento?',
'trash.delete.confirm.description': '¿Estás seguro de que deseas eliminar permanentemente este documento de la papelera? Esta acción no se puede deshacer.',
'trash.delete.confirm.label': 'Eliminar',
'trash.delete.confirm.cancel': 'Cancelar',
'trash.deleted.success.title': 'Documento eliminado',
'trash.deleted.success.description': 'El documento ha sido eliminado permanentemente.',
'activity.document.created': 'El documento ha sido creado',
'activity.document.updated.single': 'El campo {{ field }} ha sido actualizado',
'activity.document.updated.multiple': 'Los campos {{ fields }} han sido actualizados',
'activity.document.updated': 'El documento ha sido actualizado',
'activity.document.deleted': 'El documento ha sido eliminado',
'activity.document.restored': 'El documento ha sido restaurado',
'activity.document.tagged': 'La etiqueta {{ tag }} ha sido añadida',
'activity.document.untagged': 'La etiqueta {{ tag }} ha sido eliminada',
'activity.document.user.name': 'por {{ name }}',
'activity.load-more': 'Cargar más',
'activity.no-more-activities': 'No hay más actividades para este documento',
// Tags
'tags.no-tags.title': 'Aún no hay etiquetas',
'tags.no-tags.description': 'Esta organización no tiene etiquetas aún. Las etiquetas se utilizan para categorizar documentos. Puedes añadir etiquetas a tus documentos para que sean más fáciles de encontrar y organizar.',
'tags.no-tags.create-tag': 'Crear etiqueta',
'tags.title': 'Etiquetas de documentos',
'tags.description': 'Las etiquetas se utilizan para categorizar documentos. Puedes añadir etiquetas a tus documentos para que sean más fáciles de encontrar y organizar.',
'tags.create': 'Crear etiqueta',
'tags.update': 'Actualizar etiqueta',
'tags.delete': 'Eliminar etiqueta',
'tags.delete.confirm.title': 'Eliminar etiqueta',
'tags.delete.confirm.message': '¿Estás seguro de que deseas eliminar esta etiqueta? Eliminar una etiqueta la quitará de todos los documentos.',
'tags.delete.confirm.confirm-button': 'Eliminar',
'tags.delete.confirm.cancel-button': 'Cancelar',
'tags.delete.success': 'Etiqueta eliminada exitosamente',
'tags.create.success': 'Etiqueta "{{ name }}" creada exitosamente.',
'tags.update.success': 'Etiqueta "{{ name }}" actualizada exitosamente.',
'tags.form.name.label': 'Nombre',
'tags.form.name.placeholder': 'Ej. Contratos',
'tags.form.name.required': 'Por favor, ingresa un nombre para la etiqueta',
'tags.form.name.max-length': 'El nombre de la etiqueta debe tener menos de 64 caracteres',
'tags.form.color.label': 'Color',
'tags.form.color.required': 'Por favor, ingresa un color',
'tags.form.color.invalid': 'El color hexadecimal tiene un formato incorrecto.',
'tags.form.description.label': 'Descripción',
'tags.form.description.optional': '(opcional)',
'tags.form.description.placeholder': 'Ej. Todos los contratos firmados por la empresa',
'tags.form.description.max-length': 'La descripción debe tener menos de 256 caracteres',
'tags.form.no-description': 'Sin descripción',
'tags.table.headers.tag': 'Etiqueta',
'tags.table.headers.description': 'Descripción',
'tags.table.headers.documents': 'Documentos',
'tags.table.headers.created': 'Creado',
'tags.table.headers.actions': 'Acciones',
// Tagging rules
'tagging-rules.field.name': 'nombre del documento',
'tagging-rules.field.content': 'contenido del documento',
'tagging-rules.operator.equals': 'es igual a',
'tagging-rules.operator.not-equals': 'no es igual a',
'tagging-rules.operator.contains': 'contiene',
'tagging-rules.operator.not-contains': 'no contiene',
'tagging-rules.operator.starts-with': 'comienza con',
'tagging-rules.operator.ends-with': 'termina con',
'tagging-rules.list.title': 'Reglas de etiquetado',
'tagging-rules.list.description': 'Administra las reglas de etiquetado de tu organización, para etiquetar documentos automáticamente según las condiciones que definas.',
'tagging-rules.list.demo-warning': 'Nota: Como este es un entorno de demostración (sin servidor), las reglas de etiquetado no se aplicarán a los nuevos documentos añadidos.',
'tagging-rules.list.no-tagging-rules.title': 'No hay reglas de etiquetado',
'tagging-rules.list.no-tagging-rules.description': 'Crea una regla de etiquetado para etiquetar automáticamente tus documentos añadidos según las condiciones que definas.',
'tagging-rules.list.no-tagging-rules.create-tagging-rule': 'Crear regla de etiquetado',
'tagging-rules.list.card.no-conditions': 'Sin condiciones',
'tagging-rules.list.card.one-condition': '1 condición',
'tagging-rules.list.card.conditions': '{{ count }} condiciones',
'tagging-rules.list.card.delete': 'Eliminar regla',
'tagging-rules.list.card.edit': 'Editar regla',
'tagging-rules.create.title': 'Crear regla de etiquetado',
'tagging-rules.create.success': 'Regla de etiquetado creada exitosamente',
'tagging-rules.create.error': 'Error al crear la regla de etiquetado',
'tagging-rules.create.submit': 'Crear regla',
'tagging-rules.form.name.label': 'Nombre',
'tagging-rules.form.name.placeholder': 'Ejemplo: Etiquetar facturas',
'tagging-rules.form.name.min-length': 'Por favor, ingresa un nombre para la regla',
'tagging-rules.form.name.max-length': 'El nombre debe tener menos de 64 caracteres',
'tagging-rules.form.description.label': 'Descripción',
'tagging-rules.form.description.placeholder': 'Ejemplo: Etiquetar documentos con \'factura\' en el nombre',
'tagging-rules.form.description.max-length': 'La descripción debe tener menos de 256 caracteres',
'tagging-rules.form.conditions.label': 'Condiciones',
'tagging-rules.form.conditions.description': 'Define las condiciones que deben cumplirse para que la regla se aplique. Todas las condiciones deben cumplirse.',
'tagging-rules.form.conditions.add-condition': 'Añadir condición',
'tagging-rules.form.conditions.no-conditions.title': 'Sin condiciones',
'tagging-rules.form.conditions.no-conditions.description': 'No añadiste ninguna condición a esta regla. Esta regla aplicará sus etiquetas a todos los documentos.',
'tagging-rules.form.conditions.no-conditions.confirm': 'Aplicar regla sin condiciones',
'tagging-rules.form.conditions.no-conditions.cancel': 'Cancelar',
'tagging-rules.form.conditions.value.placeholder': 'Ejemplo: factura',
'tagging-rules.form.conditions.value.min-length': 'Por favor, ingresa un valor para la condición',
'tagging-rules.form.tags.label': 'Etiquetas',
'tagging-rules.form.tags.description': 'Selecciona las etiquetas a aplicar a los documentos añadidos que cumplan las condiciones',
'tagging-rules.form.tags.min-length': 'Se requiere al menos una etiqueta para aplicar',
'tagging-rules.form.tags.add-tag': 'Crear etiqueta',
'tagging-rules.form.submit': 'Crear regla',
'tagging-rules.update.title': 'Actualizar regla de etiquetado',
'tagging-rules.update.error': 'Error al actualizar la regla de etiquetado',
'tagging-rules.update.submit': 'Actualizar regla',
'tagging-rules.update.cancel': 'Cancelar',
// Intake emails
'intake-emails.title': 'Correos de ingreso',
'intake-emails.description': 'Las direcciones de correo de ingreso se usan para ingresar automáticamente correos en Papra. Solo reenvía correos a la dirección de ingreso y sus archivos adjuntos se agregarán a los documentos de tu organización.',
'intake-emails.disabled.title': 'Correos de ingreso deshabilitados',
'intake-emails.disabled.description': 'Los correos de ingreso están deshabilitados en esta instancia. Contacta a tu administrador para habilitarlos. Consulta la {{ documentation }} para más información.',
'intake-emails.disabled.documentation': 'documentación',
'intake-emails.info': 'Solo los correos de ingreso habilitados desde orígenes permitidos serán procesados. Puedes habilitar o deshabilitar un correo de ingreso en cualquier momento.',
'intake-emails.empty.title': 'Sin correos de ingreso',
'intake-emails.empty.description': 'Genera una dirección de ingreso para añadir fácilmente archivos adjuntos de correos.',
'intake-emails.empty.generate': 'Generar correo de ingreso',
'intake-emails.count': '{{ count }} correo{{ plural }} de ingreso para esta organización',
'intake-emails.new': 'Nuevo correo de ingreso',
'intake-emails.disabled-label': '(Deshabilitado)',
'intake-emails.no-origins': 'Sin orígenes de correo permitidos',
'intake-emails.allowed-origins': 'Permitido desde {{ count }} dirección{{ plural }}',
'intake-emails.actions.enable': 'Habilitar',
'intake-emails.actions.disable': 'Deshabilitar',
'intake-emails.actions.manage-origins': 'Gestionar direcciones de origen',
'intake-emails.actions.delete': 'Eliminar',
'intake-emails.delete.confirm.title': '¿Eliminar correo de ingreso?',
'intake-emails.delete.confirm.message': '¿Estás seguro de que deseas eliminar este correo de ingreso? Esta acción no se puede deshacer.',
'intake-emails.delete.confirm.confirm-button': 'Eliminar correo de ingreso',
'intake-emails.delete.confirm.cancel-button': 'Cancelar',
'intake-emails.delete.success': 'Correo de ingreso eliminado',
'intake-emails.create.success': 'Correo de ingreso creado',
'intake-emails.update.success.enabled': 'Correo de ingreso habilitado',
'intake-emails.update.success.disabled': 'Correo de ingreso deshabilitado',
'intake-emails.allowed-origins.title': 'Orígenes permitidos',
'intake-emails.allowed-origins.description': 'Solo los correos enviados a {{ email }} desde estos orígenes serán procesados. Si no se especifican orígenes, todos los correos serán descartados.',
'intake-emails.allowed-origins.add.label': 'Añadir dirección de correo permitida',
'intake-emails.allowed-origins.add.placeholder': 'Ej. ada@papra.app',
'intake-emails.allowed-origins.add.button': 'Añadir',
'intake-emails.allowed-origins.add.error.exists': 'Este correo ya está en los orígenes permitidos para este correo de ingreso',
// API keys
'api-keys.permissions.select-all': 'Seleccionar todo',
'api-keys.permissions.deselect-all': 'Deseleccionar todo',
'api-keys.permissions.organizations.title': 'Organizaciones',
'api-keys.permissions.organizations.organizations:create': 'Crear organizaciones',
'api-keys.permissions.organizations.organizations:read': 'Leer organizaciones',
'api-keys.permissions.organizations.organizations:update': 'Actualizar organizaciones',
'api-keys.permissions.organizations.organizations:delete': 'Eliminar organizaciones',
'api-keys.permissions.documents.title': 'Documentos',
'api-keys.permissions.documents.documents:create': 'Crear documentos',
'api-keys.permissions.documents.documents:read': 'Leer documentos',
'api-keys.permissions.documents.documents:update': 'Actualizar documentos',
'api-keys.permissions.documents.documents:delete': 'Eliminar documentos',
'api-keys.permissions.tags.title': 'Etiquetas',
'api-keys.permissions.tags.tags:create': 'Crear etiquetas',
'api-keys.permissions.tags.tags:read': 'Leer etiquetas',
'api-keys.permissions.tags.tags:update': 'Actualizar etiquetas',
'api-keys.permissions.tags.tags:delete': 'Eliminar etiquetas',
'api-keys.create.title': 'Crear clave API',
'api-keys.create.description': 'Crea una nueva clave API para acceder a la API de Papra.',
'api-keys.create.success': 'La clave API ha sido creada exitosamente.',
'api-keys.create.back': 'Volver a claves API',
'api-keys.create.form.name.label': 'Nombre',
'api-keys.create.form.name.placeholder': 'Ejemplo: Mi clave API',
'api-keys.create.form.name.required': 'Por favor, ingresa un nombre para la clave API',
'api-keys.create.form.permissions.label': 'Permisos',
'api-keys.create.form.permissions.required': 'Por favor, selecciona al menos un permiso',
'api-keys.create.form.submit': 'Crear clave API',
'api-keys.create.created.title': 'Clave API creada',
'api-keys.create.created.description': 'La clave API ha sido creada exitosamente. Guárdala en un lugar seguro ya que no se mostrará nuevamente.',
'api-keys.list.title': 'Claves API',
'api-keys.list.description': 'Administra tus claves API aquí.',
'api-keys.list.create': 'Crear clave API',
'api-keys.list.empty.title': 'Sin claves API',
'api-keys.list.empty.description': 'Crea una clave API para acceder a la API de Papra.',
'api-keys.list.card.last-used': 'Último uso',
'api-keys.list.card.never': 'Nunca',
'api-keys.list.card.created': 'Creado',
'api-keys.delete.success': 'La clave API ha sido eliminada exitosamente',
'api-keys.delete.confirm.title': 'Eliminar clave API',
'api-keys.delete.confirm.message': '¿Estás seguro de que deseas eliminar esta clave API? Esta acción no se puede deshacer.',
'api-keys.delete.confirm.confirm-button': 'Eliminar',
'api-keys.delete.confirm.cancel-button': 'Cancelar',
// Webhooks
'webhooks.list.title': 'Webhooks',
'webhooks.list.description': 'Administra los webhooks de tu organización',
'webhooks.list.empty.title': 'Sin webhooks',
'webhooks.list.empty.description': 'Crea tu primer webhook para empezar a recibir eventos',
'webhooks.list.create': 'Crear webhook',
'webhooks.list.card.last-triggered': 'Última activación',
'webhooks.list.card.never': 'Nunca',
'webhooks.list.card.created': 'Creado',
'webhooks.create.title': 'Crear webhook',
'webhooks.create.description': 'Crea un nuevo webhook para recibir eventos',
'webhooks.create.success': 'Webhook creado exitosamente',
'webhooks.create.back': 'Volver',
'webhooks.create.form.submit': 'Crear webhook',
'webhooks.create.form.name.label': 'Nombre del webhook',
'webhooks.create.form.name.placeholder': 'Ingresa el nombre del webhook',
'webhooks.create.form.name.required': 'El nombre es obligatorio',
'webhooks.create.form.url.label': 'URL del webhook',
'webhooks.create.form.url.placeholder': 'Ingresa la URL del webhook',
'webhooks.create.form.url.required': 'La URL es obligatoria',
'webhooks.create.form.url.invalid': 'La URL no es válida',
'webhooks.create.form.secret.label': 'Secreto',
'webhooks.create.form.secret.placeholder': 'Ingresa el secreto del webhook',
'webhooks.create.form.events.label': 'Eventos',
'webhooks.create.form.events.required': 'Se requiere al menos un evento',
'webhooks.update.title': 'Editar webhook',
'webhooks.update.description': 'Actualiza los detalles de tu webhook',
'webhooks.update.success': 'Webhook actualizado exitosamente',
'webhooks.update.submit': 'Actualizar webhook',
'webhooks.update.cancel': 'Cancelar',
'webhooks.update.form.secret.placeholder': 'Ingresa un nuevo secreto',
'webhooks.update.form.secret.placeholder-redacted': '[Secreto oculto]',
'webhooks.update.form.rotate-secret.button': 'Rotar secreto',
'webhooks.delete.success': 'Webhook eliminado exitosamente',
'webhooks.delete.confirm.title': 'Eliminar webhook',
'webhooks.delete.confirm.message': '¿Estás seguro de que deseas eliminar este webhook?',
'webhooks.delete.confirm.confirm-button': 'Eliminar',
'webhooks.delete.confirm.cancel-button': 'Cancelar',
'webhooks.events.documents.title': 'Eventos de documentos',
'webhooks.events.documents.document:created.description': 'Documento creado',
'webhooks.events.documents.document:deleted.description': 'Documento eliminado',
'webhooks.events.documents.document:updated.description': 'Documento actualizado',
'webhooks.events.documents.document:tag:added.description': 'Una etiqueta se ha añadido a un documento',
'webhooks.events.documents.document:tag:removed.description': 'Una etiqueta se ha eliminado de un documento',
// Navigation
'layout.menu.home': 'Inicio',
'layout.menu.documents': 'Documentos',
'layout.menu.tags': 'Etiquetas',
'layout.menu.tagging-rules': 'Reglas de etiquetado',
'layout.menu.deleted-documents': 'Documentos eliminados',
'layout.menu.organization-settings': 'Configuración',
'layout.menu.api-keys': 'Claves API',
'layout.menu.settings': 'Ajustes',
'layout.menu.account': 'Cuenta',
'layout.menu.general-settings': 'Ajustes generales',
'layout.menu.usage': 'Uso',
'layout.menu.intake-emails': 'Correos de ingreso',
'layout.menu.webhooks': 'Webhooks',
'layout.menu.members': 'Miembros',
'layout.menu.invitations': 'Invitaciones',
'layout.upgrade-cta.title': '¿Necesitas más espacio?',
'layout.upgrade-cta.description': 'Obtén 10x más almacenamiento + colaboración en equipo',
'layout.upgrade-cta.button': 'Actualizar ahora',
'layout.theme.light': 'Modo claro',
'layout.theme.dark': 'Modo oscuro',
'layout.theme.system': 'Modo del sistema',
'layout.search.placeholder': 'Buscar...',
'layout.menu.import-document': 'Importar un documento',
'user-menu.account-settings': 'Ajustes de cuenta',
'user-menu.api-keys': 'Claves API',
'user-menu.invitations': 'Invitaciones',
'user-menu.language': 'Idioma',
'user-menu.logout': 'Cerrar sesión',
// Command palette
'command-palette.search.placeholder': 'Buscar comandos o documentos',
'command-palette.no-results': 'No se encontraron resultados',
'command-palette.sections.documents': 'Documentos',
'command-palette.sections.theme': 'Tema',
// API errors
'api-errors.document.already_exists': 'El documento ya existe',
'api-errors.document.size_too_large': 'El archivo es demasiado grande',
'api-errors.intake-emails.already_exists': 'Ya existe un correo de ingreso con esta dirección.',
'api-errors.intake_email.limit_reached': 'Se ha alcanzado el número máximo de correos de ingreso para esta organización. Por favor, mejora tu plan para crear más correos de ingreso.',
'api-errors.user.max_organization_count_reached': 'Has alcanzado el número máximo de organizaciones que puedes crear, si necesitas crear más, contacta al soporte.',
'api-errors.default': 'Ocurrió un error al procesar tu solicitud.',
'api-errors.organization.invitation_already_exists': 'Ya existe una invitación para este correo electrónico en esta organización.',
'api-errors.user.already_in_organization': 'Este usuario ya está en esta organización.',
'api-errors.user.organization_invitation_limit_reached': 'Se ha alcanzado el número máximo de invitaciones para hoy. Por favor, inténtalo de nuevo mañana.',
'api-errors.demo.not_available': 'Esta función no está disponible en la demostración',
'api-errors.tags.already_exists': 'Ya existe una etiqueta con este nombre en esta organización',
'api-errors.internal.error': 'Ocurrió un error al procesar tu solicitud. Por favor, inténtalo de nuevo.',
'api-errors.auth.invalid_origin': 'Origen de la aplicación inválido. Si estás alojando Papra, asegúrate de que la variable de entorno APP_BASE_URL coincida con tu URL actual. Para más detalles, consulta https://docs.papra.app/resources/troubleshooting/#invalid-application-origin',
'api-errors.organization.max_members_count_reached': 'Se ha alcanzado el número máximo de miembros e invitaciones pendientes para esta organización. Por favor, actualiza tu plan para añadir más miembros.',
// Not found
'not-found.title': '404 - No encontrado',
'not-found.description': 'Lo sentimos, la página que buscas no parece existir. Por favor, verifica la URL e inténtalo de nuevo.',
'not-found.back-to-home': 'Volver al inicio',
// Demo
'demo.popup.description': 'Este es un entorno de demostración, todos los datos se guardan en el almacenamiento local de tu navegador.',
'demo.popup.discord': 'Únete a {{ discordLink }} para obtener soporte, proponer funciones o simplemente chatear.',
'demo.popup.discord-link-label': 'Servidor de Discord',
'demo.popup.reset': 'Restablecer datos de la demo',
'demo.popup.hide': 'Ocultar',
// Color picker
'color-picker.hue': 'Matiz',
'color-picker.saturation': 'Saturación',
'color-picker.lightness': 'Luminosidad',
'color-picker.select-color': 'Seleccionar color',
'color-picker.select-a-color': 'Selecciona un color',
// Subscriptions
'subscriptions.checkout-success.title': '¡Pago exitoso!',
'subscriptions.checkout-success.description': 'Tu suscripción ha sido activada exitosamente.',
'subscriptions.checkout-success.thank-you': 'Gracias por actualizar a Papra Plus. Ahora tienes acceso a todas las funciones premium.',
'subscriptions.checkout-success.go-to-organizations': 'Ir a Organizaciones',
'subscriptions.checkout-success.redirecting': 'Redirigiendo en {{ count }} segundo{{ plural }}...',
'subscriptions.checkout-cancel.title': 'Pago cancelado',
'subscriptions.checkout-cancel.description': 'Tu actualización de suscripción fue cancelada.',
'subscriptions.checkout-cancel.no-charges': 'No se han realizado cargos a tu cuenta. Puedes intentarlo de nuevo cuando estés listo.',
'subscriptions.checkout-cancel.back-to-organizations': 'Volver a Organizaciones',
'subscriptions.checkout-cancel.need-help': '¿Necesitas ayuda?',
'subscriptions.checkout-cancel.contact-support': 'Contactar soporte',
'subscriptions.upgrade-dialog.title': 'Actualizar esta organización',
'subscriptions.upgrade-dialog.description': 'Desbloquea funciones poderosas para tu organización',
'subscriptions.upgrade-dialog.contact-us': 'Contáctanos',
'subscriptions.upgrade-dialog.enterprise-plans': 'si necesitas planes empresariales personalizados.',
'subscriptions.upgrade-dialog.current-plan': 'Plan actual',
'subscriptions.upgrade-dialog.recommended': 'Recomendado',
'subscriptions.upgrade-dialog.per-month': '/mes',
'subscriptions.upgrade-dialog.billed-annually': '${{ price }} facturado anualmente',
'subscriptions.upgrade-dialog.upgrade-now': 'Actualizar ahora',
'subscriptions.plan.free.name': 'Plan gratuito',
'subscriptions.plan.plus.name': 'Plus',
'subscriptions.plan.pro.name': 'Pro',
'subscriptions.features.storage-size': 'Tamaño de almacenamiento de documentos',
'subscriptions.features.members': 'Miembros de la organización',
'subscriptions.features.members-count': '{{ count }} miembros',
'subscriptions.features.email-intakes': 'Entradas de correo',
'subscriptions.features.email-intakes-count-singular': '{{ count }} dirección',
'subscriptions.features.email-intakes-count-plural': '{{ count }} direcciones',
'subscriptions.features.max-upload-size': 'Tamaño máximo de archivo de carga',
'subscriptions.features.support': 'Soporte',
'subscriptions.features.support-community': 'Soporte de la comunidad',
'subscriptions.features.support-email': 'Soporte por correo',
'subscriptions.features.support-priority': 'Soporte prioritario',
'subscriptions.billing-interval.monthly': 'Mensual',
'subscriptions.billing-interval.annual': 'Anual',
'subscriptions.usage-warning.message': 'Ha utilizado el {{ percent }}% de su almacenamiento de documentos. Considere actualizar su plan para obtener más espacio.',
'subscriptions.usage-warning.upgrade-button': 'Actualizar plan',
// Common / Shared
'common.confirm-modal.type-to-confirm': 'Escriba "{{ text }}" para confirmar',
};

View File

@@ -0,0 +1,665 @@
import type { TranslationsDictionary } from '@/modules/i18n/locales.types';
export const translations: Partial<TranslationsDictionary> = {
// Authentication
'auth.request-password-reset.title': 'Réinitialiser votre mot de passe',
'auth.request-password-reset.description': 'Entrez votre email pour réinitialiser votre mot de passe.',
'auth.request-password-reset.requested': 'Si un compte existe pour cet email, nous vous avons envoyé un email pour réinitialiser votre mot de passe.',
'auth.request-password-reset.back-to-login': 'Retour à la connexion',
'auth.request-password-reset.form.email.label': 'Email',
'auth.request-password-reset.form.email.placeholder': 'Exemple: ada@papra.app',
'auth.request-password-reset.form.email.required': 'Veuillez entrer votre adresse email',
'auth.request-password-reset.form.email.invalid': 'Cette adresse email est invalide',
'auth.request-password-reset.form.submit': 'Réinitialiser le mot de passe',
'auth.reset-password.title': 'Réinitialiser votre mot de passe',
'auth.reset-password.description': 'Entrez votre nouveau mot de passe pour réinitialiser votre mot de passe.',
'auth.reset-password.reset': 'Votre mot de passe a été réinitialisé.',
'auth.reset-password.back-to-login': 'Retour à la connexion',
'auth.reset-password.form.new-password.label': 'Nouveau mot de passe',
'auth.reset-password.form.new-password.placeholder': 'Exemple: **********',
'auth.reset-password.form.new-password.required': 'Veuillez entrer votre nouveau mot de passe',
'auth.reset-password.form.new-password.min-length': 'Le mot de passe doit contenir au moins {{ minLength }} caractères',
'auth.reset-password.form.new-password.max-length': 'Le mot de passe doit contenir moins de {{ maxLength }} caractères',
'auth.reset-password.form.submit': 'Réinitialiser le mot de passe',
'auth.email-provider.open': 'Ouvrir {{ provider }}',
'auth.login.title': 'Connexion à Papra',
'auth.login.description': 'Entrez votre email ou utilisez une connexion sociale pour accéder à votre compte Papra.',
'auth.login.login-with-provider': 'Connexion avec {{ provider }}',
'auth.login.no-account': 'Je n\'ai pas de compte',
'auth.login.register': 'S\'inscrire',
'auth.login.form.email.label': 'Email',
'auth.login.form.email.placeholder': 'Exemple: ada@papra.app',
'auth.login.form.email.required': 'Veuillez entrer votre adresse email',
'auth.login.form.email.invalid': 'Cette adresse email est invalide',
'auth.login.form.password.label': 'Mot de passe',
'auth.login.form.password.placeholder': 'Définir un mot de passe',
'auth.login.form.password.required': 'Veuillez entrer votre mot de passe',
'auth.login.form.remember-me.label': 'Se souvenir de moi',
'auth.login.form.forgot-password.label': 'Mot de passe oublié ?',
'auth.login.form.submit': 'Connexion',
'auth.register.title': 'S\'inscrire à Papra',
'auth.register.description': 'Créez un compte pour commencer à utiliser Papra.',
'auth.register.register-with-email': 'S\'inscrire avec email',
'auth.register.register-with-provider': 'S\'inscrire avec {{ provider }}',
'auth.register.providers.google': 'Google',
'auth.register.providers.github': 'GitHub',
'auth.register.have-account': 'Je possède déjà un compte',
'auth.register.login': 'Connexion',
'auth.register.registration-disabled.title': 'Inscription désactivée',
'auth.register.registration-disabled.description': 'La création de nouveaux comptes est actuellement désactivée sur cette instance de Papra. Seuls les utilisateurs avec un compte existant peuvent se connecter. Si vous pensez que c\'est une erreur, veuillez contacter l\'administrateur de cette instance.',
'auth.register.form.email.label': 'Email',
'auth.register.form.email.placeholder': 'Exemple: ada@papra.app',
'auth.register.form.email.required': 'Veuillez entrer votre adresse email',
'auth.register.form.email.invalid': 'Cette adresse email est invalide',
'auth.register.form.password.label': 'Mot de passe',
'auth.register.form.password.placeholder': 'Définir un mot de passe',
'auth.register.form.password.required': 'Veuillez entrer votre mot de passe',
'auth.register.form.password.min-length': 'Le mot de passe doit contenir au moins {{ minLength }} caractères',
'auth.register.form.password.max-length': 'Le mot de passe doit contenir moins de {{ maxLength }} caractères',
'auth.register.form.name.label': 'Nom',
'auth.register.form.name.placeholder': 'Exemple: Ada Lovelace',
'auth.register.form.name.required': 'Veuillez entrer votre nom',
'auth.register.form.name.max-length': 'Le nom doit contenir moins de {{ maxLength }} caractères',
'auth.register.form.submit': 'S\'inscrire',
'auth.email-validation-required.title': 'Vérifier votre email',
'auth.email-validation-required.description': 'Un email de vérification a été envoyé à votre adresse email. Veuillez vérifier votre adresse email en cliquant sur le lien dans l\'email.',
'auth.legal-links.description': 'En continuant, vous reconnaissez que vous comprenez et acceptez les {{ terms }} et {{ privacy }}.',
'auth.legal-links.terms': 'Conditions d\'utilisation',
'auth.legal-links.privacy': 'Politique de confidentialité',
'auth.no-auth-provider.title': 'Aucun fournisseur d\'authentification',
'auth.no-auth-provider.description': 'Il n\'y a pas de fournisseurs d\'authentification activés sur cette instance de Papra. Veuillez contacter l\'administrateur de cette instance pour les activer.',
// User settings
'user.settings.title': 'Paramètres de l\'utilisateur',
'user.settings.description': 'Gérez vos paramètres de compte ici.',
'user.settings.email.title': 'Adresse email',
'user.settings.email.description': 'Votre adresse email ne peut pas être modifiée.',
'user.settings.email.label': 'Adresse email',
'user.settings.name.title': 'Nom complet',
'user.settings.name.description': 'Votre nom complet est affiché aux autres membres de l\'organisation.',
'user.settings.name.label': 'Nom complet',
'user.settings.name.placeholder': 'Exemple: John Doe',
'user.settings.name.update': 'Mettre à jour le nom',
'user.settings.name.updated': 'Votre nom complet a été mis à jour',
'user.settings.logout.title': 'Déconnexion',
'user.settings.logout.description': 'Déconnectez-vous de votre compte. Vous pouvez vous reconnecter plus tard.',
'user.settings.logout.button': 'Déconnexion',
// Organizations
'organizations.list.title': 'Vos organisations',
'organizations.list.description': 'Les organisations sont un moyen de grouper vos documents et de gérer l\'accès à eux. Vous pouvez créer plusieurs organisations et inviter vos membres de l\'équipe à collaborer.',
'organizations.list.create-new': 'Créer une nouvelle organisation',
'organizations.list.back': 'Retour aux organisations',
'organizations.list.deleted.title': 'Organisations supprimées',
'organizations.list.deleted.description': 'Les organisations supprimées sont conservées pendant {{ days }} jours avant d\'être définitivement supprimées. Vous pouvez les restaurer pendant cette période.',
'organizations.list.deleted.empty': 'Aucune organisation supprimée',
'organizations.list.deleted.empty-description': 'Lorsque vous supprimez une organisation, elle apparaîtra ici pendant {{ days }} jours avant d\'être définitivement supprimée.',
'organizations.list.deleted.restore': 'Restaurer',
'organizations.list.deleted.restore-success': 'Organisation restaurée avec succès',
'organizations.list.deleted.restore-confirm.title': 'Restaurer l\'organisation',
'organizations.list.deleted.restore-confirm.message': 'Êtes-vous sûr de vouloir restaurer cette organisation ? Elle sera remise dans votre liste d\'organisations actives.',
'organizations.list.deleted.restore-confirm.confirm-button': 'Restaurer l\'organisation',
'organizations.list.deleted.deleted-at': 'Supprimée le {{ date }}',
'organizations.list.deleted.purge-at': 'Sera définitivement supprimée le {{ date }}',
'organizations.list.deleted.days-remaining': '({{ daysUntilPurge, =1:{daysUntilPurge} jour, {daysUntilPurge} jours }} restant{{ daysUntilPurge, >1:s}})',
'organizations.details.no-documents.title': 'Aucun document',
'organizations.details.no-documents.description': 'Il n\'y a pas de documents dans cette organisation. Commencez par télécharger des documents.',
'organizations.details.upload-documents': 'Télécharger des documents',
'organizations.details.documents-count': 'documents en total',
'organizations.details.total-size': 'taille totale',
'organizations.details.latest-documents': 'Derniers documents importés',
'organizations.create.title': 'Créer une nouvelle organisation',
'organizations.create.description': 'Vos documents seront regroupés par organisation. Vous pouvez créer plusieurs organisations pour séparer vos documents, par exemple, pour les documents personnels et professionnels.',
'organizations.create.back': 'Retour',
'organizations.create.error.max-count-reached': 'Vous avez atteint le nombre maximum d\'organisations que vous pouvez créer, si vous avez besoin de créer plus, veuillez contacter le support.',
'organizations.create.form.name.label': 'Nom de l\'organisation',
'organizations.create.form.name.placeholder': 'Exemple: Acme Inc.',
'organizations.create.form.name.required': 'Veuillez entrer un nom pour l\'organisation',
'organizations.create.form.submit': 'Créer l\'organisation',
'organizations.create.success': 'Organisation créée avec succès',
'organizations.create-first.title': 'Créer votre organisation',
'organizations.create-first.description': 'Vos documents seront regroupés par organisation. Vous pouvez créer plusieurs organisations pour séparer vos documents, par exemple, pour les documents personnels et professionnels.',
'organizations.create-first.default-name': 'Mon organisation',
'organizations.create-first.user-name': '{{ name }}\'s organisation',
'organization.settings.title': 'Paramètres de l\'organisation',
'organization.settings.page.title': 'Paramètres de l\'organisation',
'organization.settings.page.description': 'Gérez les paramètres de votre organisation ici.',
'organization.settings.name.title': 'Nom de l\'organisation',
'organization.settings.name.update': 'Modifier le nom',
'organization.settings.name.placeholder': 'Exemple: Acme Inc.',
'organization.settings.name.updated': 'Nom de l\'organisation mis à jour',
'organization.settings.subscription.title': 'Subscription',
'organization.settings.subscription.description': 'Gérez votre facturation, vos factures et vos méthodes de paiement.',
'organization.settings.subscription.manage': 'Gérer la souscription',
'organization.settings.subscription.error': 'Échec de la récupération de l\'URL du portail client',
'organization.settings.delete.title': 'Supprimer l\'organisation',
'organization.settings.delete.description': 'Supprimer cette organisation supprimera définitivement toutes les données associées à elle.',
'organization.settings.delete.confirm.title': 'Supprimer l\'organisation',
'organization.settings.delete.confirm.message': 'Êtes-vous sûr de vouloir supprimer cette organisation ? L\'organisation sera marquée pour suppression et définitivement supprimée après {{ days }} jours. Pendant cette période, vous pouvez la restaurer depuis votre liste d\'organisations. Tous les documents et données seront définitivement supprimés après ce délai.',
'organization.settings.delete.confirm.confirm-button': 'Supprimer l\'organisation',
'organization.settings.delete.confirm.cancel-button': 'Annuler',
'organization.settings.delete.success': 'Organisation supprimée',
'organization.settings.delete.only-owner': 'Seul le propriétaire de l\'organisation peut supprimer cette organisation.',
'organization.usage.page.title': 'Utilisation',
'organization.usage.page.description': 'Consultez l\'utilisation actuelle et les limites de votre organisation.',
'organization.usage.storage.title': 'Stockage de documents',
'organization.usage.storage.description': 'Stockage total utilisé par vos documents',
'organization.usage.intake-emails.title': 'E-mails d\'ingestion',
'organization.usage.intake-emails.description': 'Nombre d\'adresses e-mail d\'ingestion',
'organization.usage.members.title': 'Membres',
'organization.usage.members.description': 'Nombre de membres dans l\'organisation',
'organization.usage.unlimited': 'Illimité',
'organizations.members.title': 'Membres',
'organizations.members.description': 'Gérez les membres de votre organisation.',
'organizations.members.invite-member': 'Inviter un membre',
'organizations.members.invite-member-disabled-tooltip': 'Seuls les administrateurs ou les propriétaires peuvent inviter des membres à l\'organisation',
'organizations.members.remove-from-organization': 'Retirer de l\'organisation',
'organizations.members.role': 'Rôle',
'organizations.members.roles.owner': 'Propriétaire',
'organizations.members.roles.admin': 'Admin',
'organizations.members.roles.member': 'Membre',
'organizations.members.delete.confirm.title': 'Retirer un membre',
'organizations.members.delete.confirm.message': 'Êtes-vous sûr de vouloir retirer ce membre de l\'organisation ?',
'organizations.members.delete.confirm.confirm-button': 'Retirer',
'organizations.members.delete.confirm.cancel-button': 'Annuler',
'organizations.members.delete.success': 'Membre retiré de l\'organisation',
'organizations.members.update-role.success': 'Rôle du membre mis à jour',
'organizations.members.table.headers.name': 'Nom',
'organizations.members.table.headers.email': 'Email',
'organizations.members.table.headers.role': 'Rôle',
'organizations.members.table.headers.created': 'Créé',
'organizations.members.table.headers.actions': 'Actions',
'organizations.invite-member.title': 'Inviter un membre',
'organizations.invite-member.description': 'Invite un membre à votre organisation',
'organizations.invite-member.form.email.label': 'Email',
'organizations.invite-member.form.email.placeholder': 'Exemple: ada@papra.app',
'organizations.invite-member.form.email.required': 'Veuillez entrer une adresse email valide',
'organizations.invite-member.form.role.label': 'Rôle',
'organizations.invite-member.form.submit': 'Inviter à l\'organisation',
'organizations.invite-member.success.message': 'Membre invité',
'organizations.invite-member.success.description': 'L\'email a été invité à l\'organisation.',
'organizations.invite-member.error.message': 'Échec de l\'invitation du membre',
'organizations.invitations.title': 'Invitations',
'organizations.invitations.description': 'Gérez les invitations de votre organisation.',
'organizations.invitations.list.cta': 'Inviter un membre',
'organizations.invitations.list.empty.title': 'Aucune invitation en attente',
'organizations.invitations.list.empty.description': 'Vous n\'avez pas été invité à aucune organisation.',
'organizations.invitations.status.pending': 'En attente',
'organizations.invitations.status.accepted': 'Accepté',
'organizations.invitations.status.rejected': 'Refusé',
'organizations.invitations.status.expired': 'Expiré',
'organizations.invitations.status.cancelled': 'Annulé',
'organizations.invitations.resend': 'Renvoyer l\'invitation',
'organizations.invitations.cancel.title': 'Annuler l\'invitation',
'organizations.invitations.cancel.description': 'Êtes-vous sûr de vouloir annuler cette invitation ?',
'organizations.invitations.cancel.confirm': 'Annuler l\'invitation',
'organizations.invitations.cancel.cancel': 'Annuler',
'organizations.invitations.resend.title': 'Renvoyer l\'invitation',
'organizations.invitations.resend.description': 'Êtes-vous sûr de vouloir renvoyer cette invitation ? Cela enverra un nouvel email à l\'invité.',
'organizations.invitations.resend.confirm': 'Renvoyer l\'invitation',
'organizations.invitations.resend.cancel': 'Annuler',
'invitations.list.title': 'Invitations',
'invitations.list.description': 'Gérez les invitations de votre organisation.',
'invitations.list.empty.title': 'Aucune invitation en attente',
'invitations.list.empty.description': 'Vous n\'avez pas été invité à aucune organisation.',
'invitations.list.headers.organization': 'Organisation',
'invitations.list.headers.status': 'Statut',
'invitations.list.headers.created': 'Créé',
'invitations.list.headers.actions': 'Actions',
'invitations.list.actions.accept': 'Accepter',
'invitations.list.actions.reject': 'Refuser',
'invitations.list.actions.accept.success.message': 'Invitation acceptée',
'invitations.list.actions.accept.success.description': 'L\'invitation a été acceptée.',
'invitations.list.actions.reject.success.message': 'Invitation refusée',
'invitations.list.actions.reject.success.description': 'L\'invitation a été refusée.',
// Documents
'documents.list.title': 'Documents',
'documents.list.no-documents.title': 'Aucun document',
'documents.list.no-documents.description': 'Il n\'y a pas de documents dans cette organisation. Commencez par télécharger des documents.',
'documents.list.no-results': 'Aucun document trouvé',
'documents.tabs.info': 'Info',
'documents.tabs.content': 'Contenu',
'documents.tabs.activity': 'Activité',
'documents.deleted.message': 'Ce document a été supprimé et sera supprimé définitivement dans {{ days }} jours.',
'documents.actions.download': 'Télécharger',
'documents.actions.open-in-new-tab': 'Ouvrir dans un nouvel onglet',
'documents.actions.restore': 'Restaurer',
'documents.actions.delete': 'Supprimer',
'documents.actions.edit': 'Modifier',
'documents.actions.cancel': 'Annuler',
'documents.actions.save': 'Enregistrer',
'documents.actions.saving': 'Enregistrement...',
'documents.content.alert': 'Le contenu du document est automatiquement extrait du document lors de l\'import. Il est uniquement utilisé pour la recherche et l\'indexation.',
'documents.content.empty-placeholder': 'Ce document n\'a pas de contenu extrait, vous pouvez le définir manuellement ici.',
'documents.info.id': 'ID',
'documents.info.name': 'Nom',
'documents.info.type': 'Type',
'documents.info.size': 'Taille',
'documents.info.created-at': 'Créé le',
'documents.info.updated-at': 'Mis à jour le',
'documents.info.never': 'Jamais',
'documents.rename.title': 'Renommer le document',
'documents.rename.form.name.label': 'Nom',
'documents.rename.form.name.placeholder': 'Exemple: Facture 2024',
'documents.rename.form.name.required': 'Veuillez entrer un nom pour le document',
'documents.rename.form.name.max-length': 'Le nom doit contenir moins de 255 caractères',
'documents.rename.form.submit': 'Renommer',
'documents.rename.success': 'Document renommé avec succès',
'documents.rename.cancel': 'Annuler',
'import-documents.title.error': '{{ count }} documents ont échoué',
'import-documents.title.success': '{{ count }} documents ont été importés',
'import-documents.title.pending': '{{ count }} / {{ total }} documents importés',
'import-documents.title.none': 'Importer des documents',
'import-documents.no-import-in-progress': 'Aucune importation de documents en cours',
'documents.deleted.title': 'Documents supprimés',
'documents.deleted.empty.title': 'Aucun document supprimé',
'documents.deleted.empty.description': 'Vous n\'avez pas de documents supprimés. Les documents supprimés seront déplacés dans la corbeille pour {{ days }} jours.',
'documents.deleted.retention-notice': 'Tous les documents supprimés sont stockés dans la corbeille pour {{ days }} jours. Passé ce délai, les documents seront supprimés définitivement, et vous ne pourrez plus les restaurer.',
'documents.deleted.deleted-at': 'Supprimé',
'documents.deleted.restoring': 'Restauration...',
'documents.deleted.deleting': 'Suppression...',
'documents.preview.unknown-file-type': 'Aucun aperçu disponible pour ce type de fichier',
'documents.preview.binary-file': 'Cela semble être un fichier binaire et ne peut pas être affiché en texte',
'trash.delete-all.button': 'Supprimer tous les documents',
'trash.delete-all.confirm.title': 'Supprimer définitivement tous les documents ?',
'trash.delete-all.confirm.description': 'Êtes-vous sûr de vouloir supprimer définitivement tous les documents de la corbeille ? Cette action est irréversible.',
'trash.delete-all.confirm.label': 'Supprimer',
'trash.delete-all.confirm.cancel': 'Annuler',
'trash.delete.button': 'Supprimer',
'trash.delete.confirm.title': 'Supprimer définitivement le document ?',
'trash.delete.confirm.description': 'Êtes-vous sûr de vouloir supprimer définitivement ce document de la corbeille ? Cette action est irréversible.',
'trash.delete.confirm.label': 'Supprimer',
'trash.delete.confirm.cancel': 'Annuler',
'trash.deleted.success.title': 'Document supprimé',
'trash.deleted.success.description': 'Le document a été supprimé définitivement.',
'activity.document.created': 'Le document a été créé',
'activity.document.updated.single': 'Le {{ field }} a été mis à jour',
'activity.document.updated.multiple': 'Les {{ fields }} ont été mis à jour',
'activity.document.updated': 'Le document a été mis à jour',
'activity.document.deleted': 'Le document a été supprimé',
'activity.document.restored': 'Le document a été restauré',
'activity.document.tagged': 'Le tag {{ tag }} a été ajouté',
'activity.document.untagged': 'Le tag {{ tag }} a été supprimé',
'activity.document.user.name': 'par {{ name }}',
'activity.load-more': 'Charger plus',
'activity.no-more-activities': 'Aucune activité pour ce document',
// Tags
'tags.no-tags.title': 'Aucun tag',
'tags.no-tags.description': 'Cette organisation n\'a pas de tags. Les tags sont utilisés pour catégoriser les documents. Vous pouvez ajouter des tags à vos documents pour les rendre plus faciles à trouver et à organiser.',
'tags.no-tags.create-tag': 'Créer un tag',
'tags.title': 'Tags de documents',
'tags.description': 'Les tags sont utilisés pour catégoriser les documents. Vous pouvez ajouter des tags à vos documents pour les rendre plus faciles à trouver et à organiser.',
'tags.create': 'Créer un tag',
'tags.update': 'Mettre à jour un tag',
'tags.delete': 'Supprimer un tag',
'tags.delete.confirm.title': 'Supprimer un tag',
'tags.delete.confirm.message': 'Êtes-vous sûr de vouloir supprimer ce tag ? Supprimer un tag supprimera toutes les règles de catégorisation qui l\'utilisent.',
'tags.delete.confirm.confirm-button': 'Supprimer',
'tags.delete.confirm.cancel-button': 'Annuler',
'tags.delete.success': 'Tag supprimé avec succès',
'tags.create.success': 'Tag "{{ name }}" créé avec succès.',
'tags.update.success': 'Tag "{{ name }}" mis à jour avec succès.',
'tags.form.name.label': 'Nom',
'tags.form.name.placeholder': 'Exemple: Contrats',
'tags.form.name.required': 'Veuillez entrer un nom pour le tag',
'tags.form.name.max-length': 'Le nom du tag doit contenir moins de 64 caractères',
'tags.form.color.label': 'Couleur',
'tags.form.color.required': 'Veuillez entrer une couleur',
'tags.form.color.invalid': 'La couleur hexadécimale est mal formatée.',
'tags.form.description.label': 'Description',
'tags.form.description.optional': '(optionnel)',
'tags.form.description.placeholder': 'Exemple: Tous les contrats signés par l\'entreprise',
'tags.form.description.max-length': 'La description doit contenir moins de 256 caractères',
'tags.form.no-description': 'Aucune description',
'tags.table.headers.tag': 'Tag',
'tags.table.headers.description': 'Description',
'tags.table.headers.documents': 'Documents',
'tags.table.headers.created': 'Date de création',
'tags.table.headers.actions': 'Actions',
// Tagging rules
'tagging-rules.field.name': 'nom du document',
'tagging-rules.field.content': 'contenu du document',
'tagging-rules.operator.equals': 'égal à',
'tagging-rules.operator.not-equals': 'différent de',
'tagging-rules.operator.contains': 'contient',
'tagging-rules.operator.not-contains': 'ne contient pas',
'tagging-rules.operator.starts-with': 'commence par',
'tagging-rules.operator.ends-with': 'finit par',
'tagging-rules.list.title': 'Règles de catégorisation',
'tagging-rules.list.description': 'Gérez vos règles de catégorisation, pour catégoriser automatiquement les documents en fonction de conditions que vous définissez.',
'tagging-rules.list.demo-warning': 'Note: Cette instance est une démo, les règles de catégorisation ne seront pas appliquées aux documents ajoutés.',
'tagging-rules.list.no-tagging-rules.title': 'Aucune règle de catégorisation',
'tagging-rules.list.no-tagging-rules.description': 'Créez une règle de catégorisation pour catégoriser automatiquement vos documents en fonction de conditions que vous définissez.',
'tagging-rules.list.no-tagging-rules.create-tagging-rule': 'Créer une règle de catégorisation',
'tagging-rules.list.card.no-conditions': 'Aucune condition',
'tagging-rules.list.card.one-condition': '1 condition',
'tagging-rules.list.card.conditions': '{{ count }} conditions',
'tagging-rules.list.card.delete': 'Supprimer la règle',
'tagging-rules.list.card.edit': 'Modifier la règle',
'tagging-rules.create.title': 'Créer une règle de catégorisation',
'tagging-rules.create.success': 'Règle de catégorisation créée avec succès',
'tagging-rules.create.error': 'Échec de la création de la règle de catégorisation',
'tagging-rules.create.submit': 'Créer la règle',
'tagging-rules.form.name.label': 'Nom',
'tagging-rules.form.name.placeholder': 'Exemple: Catégoriser les factures',
'tagging-rules.form.name.min-length': 'Veuillez entrer un nom pour la règle',
'tagging-rules.form.name.max-length': 'Le nom doit contenir moins de 64 caractères',
'tagging-rules.form.description.label': 'Description',
'tagging-rules.form.description.placeholder': 'Exemple: Catégoriser les documents avec \'facture\' dans le nom',
'tagging-rules.form.description.max-length': 'La description doit contenir moins de 256 caractères',
'tagging-rules.form.conditions.label': 'Conditions',
'tagging-rules.form.conditions.description': 'Définissez les conditions que doivent remplir la règle pour qu\'elle s\'applique. Toutes les conditions doivent être remplies pour que la règle s\'applique.',
'tagging-rules.form.conditions.add-condition': 'Ajouter une condition',
'tagging-rules.form.conditions.no-conditions.title': 'Aucune condition',
'tagging-rules.form.conditions.no-conditions.description': 'Vous n\'avez pas ajouté de conditions à cette règle. Cette règle appliquera ses tags à tous les documents.',
'tagging-rules.form.conditions.no-conditions.confirm': 'Appliquer la règle sans conditions',
'tagging-rules.form.conditions.no-conditions.cancel': 'Annuler',
'tagging-rules.form.conditions.value.placeholder': 'Exemple: facture',
'tagging-rules.form.conditions.value.min-length': 'Veuillez entrer une valeur pour la condition',
'tagging-rules.form.tags.label': 'Tags',
'tagging-rules.form.tags.description': 'Sélectionnez les tags à appliquer aux documents ajoutés qui correspondent aux conditions',
'tagging-rules.form.tags.min-length': 'Au moins un tag à appliquer est requis',
'tagging-rules.form.tags.add-tag': 'Créer un tag',
'tagging-rules.form.submit': 'Créer la règle',
'tagging-rules.update.title': 'Mettre à jour la règle de catégorisation',
'tagging-rules.update.error': 'Échec de la mise à jour de la règle de catégorisation',
'tagging-rules.update.submit': 'Mettre à jour la règle',
'tagging-rules.update.cancel': 'Annuler',
// Intake emails
'intake-emails.title': 'Adresses de réception',
'intake-emails.description': 'Les adresses de réception sont utilisées pour ingérer automatiquement les emails dans Papra. Il suffit de les envoyer à l\'adresse de réception et leurs pièces jointes seront ajoutées à vos documents.',
'intake-emails.disabled.title': 'Les adresses de réception sont désactivées',
'intake-emails.disabled.description': 'Les adresses de réception sont désactivées sur cette instance. Veuillez contacter votre administrateur pour les activer. Voir la {{ documentation }} pour plus d\'informations.',
'intake-emails.disabled.documentation': 'documentation',
'intake-emails.info': 'Seules les adresses de réception activées depuis les origines autorisées seront traitées. Vous pouvez activer ou désactiver une adresse de réception à tout moment.',
'intake-emails.empty.title': 'Aucune adresse de réception',
'intake-emails.empty.description': 'Générez une adresse de réception pour ingérer facilement les pièces jointes des emails.',
'intake-emails.empty.generate': 'Générer une adresse de réception',
'intake-emails.count': '{{ count }} intake email{{ plural }} for this organization',
'intake-emails.new': 'Nouvelle adresse de réception',
'intake-emails.disabled-label': '(Désactivé)',
'intake-emails.no-origins': 'Aucune adresse de réception autorisée',
'intake-emails.allowed-origins': 'Autorisées depuis {{ count }} adresse{{ plural }}',
'intake-emails.actions.enable': 'Activer',
'intake-emails.actions.disable': 'Désactiver',
'intake-emails.actions.manage-origins': 'Gérer les adresses d\'origine',
'intake-emails.actions.delete': 'Supprimer',
'intake-emails.delete.confirm.title': 'Supprimer l\'adresse de réception ?',
'intake-emails.delete.confirm.message': 'Êtes-vous sûr de vouloir supprimer cette adresse de réception ? Cette action est irréversible.',
'intake-emails.delete.confirm.confirm-button': 'Supprimer l\'adresse de réception',
'intake-emails.delete.confirm.cancel-button': 'Annuler',
'intake-emails.delete.success': 'Adresse de réception supprimée',
'intake-emails.create.success': 'Adresse de réception créée',
'intake-emails.update.success.enabled': 'Adresse de réception activée',
'intake-emails.update.success.disabled': 'Adresse de réception désactivée',
'intake-emails.allowed-origins.title': 'Adresses d\'origine autorisées',
'intake-emails.allowed-origins.description': 'Seuls les emails envoyés à {{ email }} depuis ces adresses d\'origine seront traités. Si aucune adresse d\'origine n\'est spécifiée, tous les emails seront rejetés.',
'intake-emails.allowed-origins.add.label': 'Ajouter une adresse d\'origine autorisée',
'intake-emails.allowed-origins.add.placeholder': 'Exemple: ada@papra.app',
'intake-emails.allowed-origins.add.button': 'Ajouter',
'intake-emails.allowed-origins.add.error.exists': 'Cette adresse email est déjà dans les adresses d\'origine autorisées pour cette adresse de réception',
// API keys
'api-keys.permissions.select-all': 'Tout sélectionner',
'api-keys.permissions.deselect-all': 'Tout désélectionner',
'api-keys.permissions.organizations.title': 'Organisations',
'api-keys.permissions.organizations.organizations:create': 'Créer des organisations',
'api-keys.permissions.organizations.organizations:read': 'Lire des organisations',
'api-keys.permissions.organizations.organizations:update': 'Mettre à jour des organisations',
'api-keys.permissions.organizations.organizations:delete': 'Supprimer des organisations',
'api-keys.permissions.documents.title': 'Documents',
'api-keys.permissions.documents.documents:create': 'Créer des documents',
'api-keys.permissions.documents.documents:read': 'Lire des documents',
'api-keys.permissions.documents.documents:update': 'Mettre à jour des documents',
'api-keys.permissions.documents.documents:delete': 'Supprimer des documents',
'api-keys.permissions.tags.title': 'Tags',
'api-keys.permissions.tags.tags:create': 'Créer des tags',
'api-keys.permissions.tags.tags:read': 'Lire des tags',
'api-keys.permissions.tags.tags:update': 'Mettre à jour des tags',
'api-keys.permissions.tags.tags:delete': 'Supprimer des tags',
'api-keys.create.title': 'Créer une clé API',
'api-keys.create.description': 'Créer une nouvelle clé API pour accéder à l\'API de Papra.',
'api-keys.create.success': 'La clé API a été créée avec succès.',
'api-keys.create.back': 'Retour aux clés API',
'api-keys.create.form.name.label': 'Nom',
'api-keys.create.form.name.placeholder': 'Exemple: Ma clé API',
'api-keys.create.form.name.required': 'Veuillez entrer un nom pour la clé API',
'api-keys.create.form.permissions.label': 'Permissions',
'api-keys.create.form.permissions.required': 'Veuillez sélectionner au moins une permission',
'api-keys.create.form.submit': 'Créer la clé API',
'api-keys.create.created.title': 'Clé API créée',
'api-keys.create.created.description': 'La clé API a été créée avec succès. Enregistrez-la dans un endroit sûr car elle ne sera plus affichée.',
'api-keys.list.title': 'Clés API',
'api-keys.list.description': 'Gérez vos clés API ici.',
'api-keys.list.create': 'Créer une clé API',
'api-keys.list.empty.title': 'Aucune clé API',
'api-keys.list.empty.description': 'Créez une clé API pour accéder à l\'API de Papra.',
'api-keys.list.card.last-used': 'Dernière utilisation',
'api-keys.list.card.never': 'Jamais',
'api-keys.list.card.created': 'Créée',
'api-keys.delete.success': 'La clé API a été supprimée avec succès',
'api-keys.delete.confirm.title': 'Supprimer la clé API',
'api-keys.delete.confirm.message': 'Êtes-vous sûr de vouloir supprimer cette clé API ? Cette action est irréversible.',
'api-keys.delete.confirm.confirm-button': 'Supprimer',
'api-keys.delete.confirm.cancel-button': 'Annuler',
// Webhooks
'webhooks.list.title': 'Webhooks',
'webhooks.list.description': 'Gérez vos webhooks ici.',
'webhooks.list.empty.title': 'Aucun webhook',
'webhooks.list.empty.description': 'Créez votre premier webhook pour commencer à recevoir des événements.',
'webhooks.list.create': 'Créer un webhook',
'webhooks.list.card.last-triggered': 'Dernière invocation',
'webhooks.list.card.never': 'Jamais',
'webhooks.list.card.created': 'Créée',
'webhooks.create.title': 'Créer un webhook',
'webhooks.create.description': 'Créez un webhook pour recevoir des événements lorsque des documents sont ajoutés à votre organisation.',
'webhooks.create.success': 'Le webhook a été créé avec succès.',
'webhooks.create.back': 'Retour aux webhooks',
'webhooks.create.form.submit': 'Créer le webhook',
'webhooks.create.form.name.label': 'Nom du webhook',
'webhooks.create.form.name.placeholder': 'Entrez le nom du webhook',
'webhooks.create.form.name.required': 'Le nom est requis',
'webhooks.create.form.url.label': 'URL du webhook',
'webhooks.create.form.url.placeholder': 'Entrez l\'URL du webhook',
'webhooks.create.form.url.required': 'L\'URL est requise',
'webhooks.create.form.url.invalid': 'L\'URL est invalide',
'webhooks.create.form.secret.label': 'Secret',
'webhooks.create.form.secret.placeholder': 'Entrez le secret du webhook',
'webhooks.create.form.events.label': 'Événements',
'webhooks.create.form.events.required': 'Au moins un événement est requis',
'webhooks.update.title': 'Modifier le webhook',
'webhooks.update.description': 'Mettez à jour les détails de votre webhook',
'webhooks.update.success': 'Le webhook a été mis à jour avec succès',
'webhooks.update.submit': 'Mettre à jour le webhook',
'webhooks.update.cancel': 'Annuler',
'webhooks.update.form.secret.placeholder': 'Entrez un nouveau secret',
'webhooks.update.form.secret.placeholder-redacted': '[Secret masqué]',
'webhooks.update.form.rotate-secret.button': 'Rotation du secret',
'webhooks.delete.success': 'Le webhook a été supprimé avec succès',
'webhooks.delete.confirm.title': 'Supprimer le webhook',
'webhooks.delete.confirm.message': 'Êtes-vous sûr de vouloir supprimer ce webhook ? Cette action est irréversible.',
'webhooks.delete.confirm.confirm-button': 'Supprimer',
'webhooks.delete.confirm.cancel-button': 'Annuler',
'webhooks.events.documents.title': 'Événements de documents',
'webhooks.events.documents.document:created.description': 'Document créé',
'webhooks.events.documents.document:deleted.description': 'Document supprimé',
'webhooks.events.documents.document:updated.description': 'Document mis à jour',
'webhooks.events.documents.document:tag:added.description': 'Un tag est ajouté à un document',
'webhooks.events.documents.document:tag:removed.description': 'Un tag est retiré d\'un document',
// Navigation
'layout.menu.home': 'Accueil',
'layout.menu.documents': 'Documents',
'layout.menu.tags': 'Tags',
'layout.menu.tagging-rules': 'Règles de catégorisation',
'layout.menu.deleted-documents': 'Documents supprimés',
'layout.menu.organization-settings': 'Paramètres',
'layout.menu.api-keys': 'API keys',
'layout.menu.settings': 'Paramètres',
'layout.menu.account': 'Compte',
'layout.menu.general-settings': 'Paramètres généraux',
'layout.menu.usage': 'Utilisation',
'layout.menu.intake-emails': 'Adresses de réception',
'layout.menu.webhooks': 'Webhooks',
'layout.menu.members': 'Membres',
'layout.menu.invitations': 'Invitations',
'layout.upgrade-cta.title': 'Besoin de plus d\'espace ?',
'layout.upgrade-cta.description': 'Obtenez 10x plus de stockage + collaboration d\'équipe',
'layout.upgrade-cta.button': 'Mettre à niveau maintenant',
'layout.theme.light': 'Mode clair',
'layout.theme.dark': 'Mode sombre',
'layout.theme.system': 'Mode système',
'layout.search.placeholder': 'Rechercher...',
'layout.menu.import-document': 'Importer un document',
'user-menu.account-settings': 'Paramètres du compte',
'user-menu.api-keys': 'Clés d\'API',
'user-menu.invitations': 'Invitations',
'user-menu.language': 'Langue',
'user-menu.logout': 'Déconnexion',
// Command palette
'command-palette.search.placeholder': 'Rechercher des commandes ou des documents',
'command-palette.no-results': 'Aucun résultat trouvé',
'command-palette.sections.documents': 'Documents',
'command-palette.sections.theme': 'Thème',
// API errors
'api-errors.document.already_exists': 'Le document existe déjà',
'api-errors.document.size_too_large': 'Le fichier est trop volumineux',
'api-errors.intake-emails.already_exists': 'Un email de réception avec cette adresse existe déjà.',
'api-errors.intake_email.limit_reached': 'Le nombre maximum d\'emails de réception pour cette organisation a été atteint. Veuillez mettre à niveau votre plan pour créer plus d\'emails de réception.',
'api-errors.user.max_organization_count_reached': 'Vous avez atteint le nombre maximum d\'organisations que vous pouvez créer, si vous avez besoin de créer plus, veuillez contacter le support.',
'api-errors.default': 'Une erreur est survenue lors du traitement de votre requête.',
'api-errors.organization.invitation_already_exists': 'Une invitation pour cet email existe déjà dans cette organisation.',
'api-errors.user.already_in_organization': 'Cet utilisateur est déjà dans cette organisation.',
'api-errors.user.organization_invitation_limit_reached': 'Le nombre maximum d\'invitations a été atteint pour aujourd\'hui. Veuillez réessayer demain.',
'api-errors.demo.not_available': 'Cette fonctionnalité n\'est pas disponible dans la démo',
'api-errors.tags.already_exists': 'Un tag avec ce nom existe déjà pour cette organisation',
'api-errors.internal.error': 'Une erreur est survenue lors du traitement de votre requête. Veuillez réessayer.',
'api-errors.auth.invalid_origin': 'Origine de l\'application invalide. Si vous hébergez Papra, assurez-vous que la variable d\'environnement APP_BASE_URL correspond à votre URL actuelle. Pour plus de détails, consultez https://docs.papra.app/resources/troubleshooting/#invalid-application-origin',
'api-errors.organization.max_members_count_reached': 'Le nombre maximum de membres et d\'invitations en attente pour cette organisation a été atteint. Veuillez mettre à niveau votre plan pour ajouter plus de membres.',
// Not found
'not-found.title': '404 - Not Found',
'not-found.description': 'Désolé, la page que vous cherchez n\'existe pas. Veuillez vérifier l\'URL et réessayer.',
'not-found.back-to-home': 'Retour à l\'accueil',
// Demo
'demo.popup.description': 'Cette instance est une démo, toutes les données sont sauvegardées dans le stockage local de votre navigateur.',
'demo.popup.discord': 'Rejoignez le {{ discordLink }} pour obtenir de l\'aide, proposer des fonctionnalités ou simplement discuter.',
'demo.popup.discord-link-label': 'Serveur Discord',
'demo.popup.reset': 'Réinitialiser la démo',
'demo.popup.hide': 'Masquer',
// Color picker
'color-picker.hue': 'Teinte',
'color-picker.saturation': 'Saturation',
'color-picker.lightness': 'Luminosité',
'color-picker.select-color': 'Sélectionner la couleur',
'color-picker.select-a-color': 'Sélectionner une couleur',
// Subscriptions
'subscriptions.checkout-success.title': 'Paiement réussi !',
'subscriptions.checkout-success.description': 'Votre abonnement a été activé avec succès.',
'subscriptions.checkout-success.thank-you': 'Merci d\'avoir mis à niveau vers Papra Plus. Vous avez maintenant accès à toutes les fonctionnalités premium.',
'subscriptions.checkout-success.go-to-organizations': 'Aller aux Organisations',
'subscriptions.checkout-success.redirecting': 'Redirection dans {{ count }} seconde{{ plural }}...',
'subscriptions.checkout-cancel.title': 'Paiement annulé',
'subscriptions.checkout-cancel.description': 'Votre mise à niveau d\'abonnement a été annulée.',
'subscriptions.checkout-cancel.no-charges': 'Aucun frais n\'a été prélevé sur votre compte. Vous pouvez réessayer à tout moment.',
'subscriptions.checkout-cancel.back-to-organizations': 'Retour aux Organisations',
'subscriptions.checkout-cancel.need-help': 'Besoin d\'aide ?',
'subscriptions.checkout-cancel.contact-support': 'Contacter le support',
'subscriptions.upgrade-dialog.title': 'Mettre à niveau cette organisation',
'subscriptions.upgrade-dialog.description': 'Débloquez des fonctionnalités puissantes pour votre organisation',
'subscriptions.upgrade-dialog.contact-us': 'Contactez-nous',
'subscriptions.upgrade-dialog.enterprise-plans': 'si vous avez besoin de plans d\'entreprise personnalisés.',
'subscriptions.upgrade-dialog.current-plan': 'Plan actuel',
'subscriptions.upgrade-dialog.recommended': 'Recommandé',
'subscriptions.upgrade-dialog.per-month': '/mois',
'subscriptions.upgrade-dialog.billed-annually': '${{ price }} facturé annuellement',
'subscriptions.upgrade-dialog.upgrade-now': 'Mettre à niveau',
'subscriptions.plan.free.name': 'Plan gratuit',
'subscriptions.plan.plus.name': 'Plus',
'subscriptions.plan.pro.name': 'Pro',
'subscriptions.features.storage-size': 'Taille de stockage de documents',
'subscriptions.features.members': 'Membres de l\'organisation',
'subscriptions.features.members-count': '{{ count }} membres',
'subscriptions.features.email-intakes': 'Emails de réception',
'subscriptions.features.email-intakes-count-singular': '{{ count }} adresse',
'subscriptions.features.email-intakes-count-plural': '{{ count }} adresses',
'subscriptions.features.max-upload-size': 'Taille maximale de téléchargement',
'subscriptions.features.support': 'Support',
'subscriptions.features.support-community': 'Support communautaire',
'subscriptions.features.support-email': 'Support par email',
'subscriptions.features.support-priority': 'Support prioritaire',
'subscriptions.billing-interval.monthly': 'Mensuel',
'subscriptions.billing-interval.annual': 'Annuel',
'subscriptions.usage-warning.message': 'Vous avez utilisé {{ percent }}% de votre stockage de documents. Envisagez de mettre à niveau votre plan pour obtenir plus d\'espace.',
'subscriptions.usage-warning.upgrade-button': 'Mettre à niveau',
// Common / Shared
'common.confirm-modal.type-to-confirm': 'Saisissez "{{ text }}" pour confirmer',
};

View File

@@ -1,202 +0,0 @@
auth.request-password-reset.title: Réinitialiser votre mot de passe
auth.request-password-reset.description: Entrez votre email pour réinitialiser votre mot de passe.
auth.request-password-reset.requested: Si un compte existe pour cet email, nous vous avons envoyé un email pour réinitialiser votre mot de passe.
auth.request-password-reset.back-to-login: Retour à la connexion
auth.request-password-reset.form.email.label: Email
auth.request-password-reset.form.email.placeholder: 'Exemple: ada@papra.app'
auth.request-password-reset.form.email.required: Veuillez entrer votre adresse email
auth.request-password-reset.form.email.invalid: Cette adresse email est invalide
auth.request-password-reset.form.submit: Réinitialiser le mot de passe
auth.reset-password.title: Réinitialiser votre mot de passe
auth.reset-password.description: Entrez votre nouveau mot de passe pour réinitialiser votre mot de passe.
auth.reset-password.reset: Votre mot de passe a été réinitialisé.
auth.reset-password.back-to-login: Retour à la connexion
auth.reset-password.form.new-password.label: Nouveau mot de passe
auth.reset-password.form.new-password.placeholder: 'Exemple: **********'
auth.reset-password.form.new-password.required: Veuillez entrer votre nouveau mot de passe
auth.reset-password.form.new-password.min-length: Le mot de passe doit contenir au moins {{ minLength }} caractères
auth.reset-password.form.new-password.max-length: Le mot de passe doit contenir moins de {{ maxLength }} caractères
auth.reset-password.form.submit: Réinitialiser le mot de passe
auth.email-provider.open: Ouvrir {{ provider }}
auth.login.title: Connexion à Papra
auth.login.description: Entrez votre email ou utilisez une connexion sociale pour accéder à votre compte Papra.
auth.login.login-with-provider: Connexion avec {{ provider }}
auth.login.no-account: Je n'ai pas de compte
auth.login.register: S'inscrire
auth.login.form.email.label: Email
auth.login.form.email.placeholder: 'Exemple: ada@papra.app'
auth.login.form.email.required: Veuillez entrer votre adresse email
auth.login.form.email.invalid: Cette adresse email est invalide
auth.login.form.password.label: Mot de passe
auth.login.form.password.placeholder: Définir un mot de passe
auth.login.form.password.required: Veuillez entrer votre mot de passe
auth.login.form.remember-me.label: Se souvenir de moi
auth.login.form.forgot-password.label: Mot de passe oublié ?
auth.login.form.submit: Connexion
auth.register.title: S'inscrire à Papra
auth.register.description: Entrez votre email ou utilisez une connexion sociale pour accéder à votre compte Papra.
auth.register.register-with-email: S'inscrire avec email
auth.register.register-with-provider: S'inscrire avec {{ provider }}
auth.register.providers.google: Google
auth.register.providers.github: GitHub
auth.register.have-account: Je possède déjà un compte
auth.register.login: Connexion
auth.register.registration-disabled.title: Inscription désactivée
auth.register.registration-disabled.description: La création de nouveaux comptes est actuellement désactivée sur cette instance de Papra. Seuls les utilisateurs avec un compte existant peuvent se connecter. Si vous pensez que c'est une erreur, veuillez contacter l'administrateur de cette instance.
auth.register.form.email.label: Email
auth.register.form.email.placeholder: 'Exemple: ada@papra.app'
auth.register.form.email.required: Veuillez entrer votre adresse email
auth.register.form.email.invalid: Cette adresse email est invalide
auth.register.form.password.label: Mot de passe
auth.register.form.password.placeholder: Définir un mot de passe
auth.register.form.password.required: Veuillez entrer votre mot de passe
auth.register.form.password.min-length: Le mot de passe doit contenir au moins {{ minLength }} caractères
auth.register.form.password.max-length: Le mot de passe doit contenir moins de {{ maxLength }} caractères
auth.register.form.name.label: Nom
auth.register.form.name.placeholder: 'Exemple: Ada Lovelace'
auth.register.form.name.required: Veuillez entrer votre nom
auth.register.form.name.max-length: Le nom doit contenir moins de {{ maxLength }} caractères
auth.register.form.submit: S'inscrire
auth.email-validation-required.title: Vérifier votre email
auth.email-validation-required.description: Un email de vérification a été envoyé à votre adresse email. Veuillez vérifier votre adresse email en cliquant sur le lien dans l'email.
auth.legal-links.description: En continuant, vous reconnaissez que vous comprenez et acceptez les {{ terms }} et {{ privacy }}.
auth.legal-links.terms: Conditions d'utilisation
auth.legal-links.privacy: Politique de confidentialité
tags.no-tags.title: Aucun tag
tags.no-tags.description: Cette organisation n'a pas de tags. Les tags sont utilisés pour catégoriser les documents. Vous pouvez ajouter des tags à vos documents pour les rendre plus faciles à trouver et à organiser.
tags.no-tags.create-tag: Créer un tag
layout.menu.home: Accueil
layout.menu.documents: Documents
layout.menu.tags: Tags
layout.menu.tagging-rules: Règles de catégorisation
layout.menu.deleted-documents: Documents supprimés
layout.menu.organization-settings: Paramètres
layout.menu.api-keys: API keys
layout.menu.settings: Paramètres
layout.menu.account: Compte
tagging-rules.field.name: nom du document
tagging-rules.field.content: contenu du document
tagging-rules.operator.equals: égal à
tagging-rules.operator.not-equals: différent de
tagging-rules.operator.contains: contient
tagging-rules.operator.not-contains: ne contient pas
tagging-rules.operator.starts-with: commence par
tagging-rules.operator.ends-with: finit par
tagging-rules.list.title: Règles de catégorisation
tagging-rules.list.description: Gérez vos règles de catégorisation, pour catégoriser automatiquement les documents en fonction de conditions que vous définissez.
tagging-rules.list.demo-warning: 'Note: Cette instance est une démo, les règles de catégorisation ne seront pas appliquées aux documents ajoutés.'
tagging-rules.list.no-tagging-rules.title: Aucune règle de catégorisation
tagging-rules.list.no-tagging-rules.description: Créez une règle de catégorisation pour catégoriser automatiquement vos documents en fonction de conditions que vous définissez.
tagging-rules.list.no-tagging-rules.create-tagging-rule: Créer une règle de catégorisation
tagging-rules.list.card.no-conditions: Aucune condition
tagging-rules.list.card.one-condition: 1 condition
tagging-rules.list.card.conditions: '{{ count }} conditions'
tagging-rules.list.card.delete: Supprimer la règle
tagging-rules.list.card.edit: Modifier la règle
tagging-rules.create.title: Créer une règle de catégorisation
tagging-rules.create.success: Règle de catégorisation créée avec succès
tagging-rules.create.error: Échec de la création de la règle de catégorisation
tagging-rules.create.submit: Créer la règle
tagging-rules.form.name.label: Nom
tagging-rules.form.name.placeholder: 'Exemple: Catégoriser les factures'
tagging-rules.form.name.min-length: Veuillez entrer un nom pour la règle
tagging-rules.form.name.max-length: Le nom doit contenir moins de 64 caractères
tagging-rules.form.description.label: Description
tagging-rules.form.description.placeholder: "Exemple: Catégoriser les documents avec 'facture' dans le nom"
tagging-rules.form.description.max-length: La description doit contenir moins de 256 caractères
tagging-rules.form.conditions.label: Conditions
tagging-rules.form.conditions.description: Définissez les conditions que doivent remplir la règle pour qu'elle s'applique. Toutes les conditions doivent être remplies pour que la règle s'applique.
tagging-rules.form.conditions.add-condition: Ajouter une condition
tagging-rules.form.conditions.no-conditions.title: Aucune condition
tagging-rules.form.conditions.no-conditions.description: Vous n'avez pas ajouté de conditions à cette règle. Cette règle appliquera ses tags à tous les documents.
tagging-rules.form.conditions.no-conditions.confirm: Appliquer la règle sans conditions
tagging-rules.form.conditions.no-conditions.cancel: Annuler
tagging-rules.form.conditions.value.placeholder: 'Exemple: facture'
tagging-rules.form.conditions.value.min-length: Veuillez entrer une valeur pour la condition
tagging-rules.form.tags.label: Tags
tagging-rules.form.tags.description: Sélectionnez les tags à appliquer aux documents ajoutés qui correspondent aux conditions
tagging-rules.form.tags.min-length: Au moins un tag à appliquer est requis
tagging-rules.form.tags.add-tag: Créer un tag
tagging-rules.form.submit: Créer la règle
tagging-rules.update.title: Mettre à jour la règle de catégorisation
tagging-rules.update.error: Échec de la mise à jour de la règle de catégorisation
tagging-rules.update.submit: Mettre à jour la règle
tagging-rules.update.cancel: Annuler
demo.popup.description: Cette instance est une démo, toutes les données sont sauvegardées dans le stockage local de votre navigateur.
demo.popup.discord: Rejoignez le {{ discordLink }} pour obtenir de l'aide, proposer des fonctionnalités ou simplement discuter.
demo.popup.discord-link-label: Serveur Discord
demo.popup.reset: Réinitialiser les données de la démo
demo.popup.hide: Masquer
trash.delete-all.button: Supprimer tous les documents
trash.delete-all.confirm.title: Supprimer définitivement tous les documents ?
trash.delete-all.confirm.description: Êtes-vous sûr de vouloir supprimer définitivement tous les documents de la corbeille ? Cette action est irréversible.
trash.delete-all.confirm.label: Supprimer
trash.delete-all.confirm.cancel: Annuler
trash.delete.button: Supprimer
trash.delete.confirm.title: Supprimer définitivement le document ?
trash.delete.confirm.description: Êtes-vous sûr de vouloir supprimer définitivement ce document de la corbeille ? Cette action est irréversible.
trash.delete.confirm.label: Supprimer
trash.delete.confirm.cancel: Annuler
trash.deleted.success.title: Document supprimé
trash.deleted.success.description: Le document a été supprimé définitivement.
import-documents.title.error: '{{ count }} documents ont échoué'
import-documents.title.success: '{{ count }} documents ont été importés'
import-documents.title.pending: '{{ count }} / {{ total }} documents importés'
import-documents.title.none: Importer des documents
import-documents.no-import-in-progress: Aucune importation de documents en cours
api-errors.document.already_exists: Le document existe déjà
api-errors.document.file_too_big: Le fichier du document est trop grand
api-errors.intake_email.limit_reached: Le nombre maximum d'emails de réception pour cette organisation a été atteint. Veuillez mettre à niveau votre plan pour créer plus d'emails de réception.
api-errors.user.max_organization_count_reached: Vous avez atteint le nombre maximum d'organisations que vous pouvez créer, si vous avez besoin de créer plus, veuillez contacter le support.
api-errors.default: Une erreur est survenue lors du traitement de votre requête.
api-keys.permissions.documents.title: Documents
api-keys.permissions.documents.documents:create: Créer des documents
api-keys.permissions.documents.documents:read: Lire des documents
api-keys.permissions.documents.documents:update: Mettre à jour des documents
api-keys.permissions.documents.documents:delete: Supprimer des documents
api-keys.permissions.tags.title: Tags
api-keys.permissions.tags.tags:create: Créer des tags
api-keys.permissions.tags.tags:read: Lire des tags
api-keys.permissions.tags.tags:update: Mettre à jour des tags
api-keys.permissions.tags.tags:delete: Supprimer des tags
api-keys.create.title: Créer une clé API
api-keys.create.description: Créer une nouvelle clé API pour accéder à l'API de Papra.
api-keys.create.success: La clé API a été créée avec succès.
api-keys.create.back: Retour aux clés API
api-keys.create.form.name.label: Nom
api-keys.create.form.name.placeholder: 'Exemple: Ma clé API'
api-keys.create.form.name.required: Veuillez entrer un nom pour la clé API
api-keys.create.form.permissions.label: Permissions
api-keys.create.form.permissions.required: Veuillez sélectionner au moins une permission
api-keys.create.form.submit: Créer la clé API
api-keys.create.created.title: Clé API créée
api-keys.create.created.description: La clé API a été créée avec succès. Enregistrez-la dans un endroit sûr car elle ne sera plus affichée.
api-keys.list.title: Clés API
api-keys.list.description: Gérez vos clés API ici.
api-keys.list.create: Créer une clé API
api-keys.list.empty.title: Aucune clé API
api-keys.list.empty.description: Créez une clé API pour accéder à l'API de Papra.
api-keys.list.card.last-used: Dernière utilisation
api-keys.list.card.never: Jamais
api-keys.list.card.created: Créée
api-keys.delete.success: La clé API a été supprimée avec succès
api-keys.delete.confirm.title: Supprimer la clé API
api-keys.delete.confirm.message: Êtes-vous sûr de vouloir supprimer cette clé API ? Cette action est irréversible.
api-keys.delete.confirm.confirm-button: Supprimer
api-keys.delete.confirm.cancel-button: Annuler

View File

@@ -0,0 +1,665 @@
import type { TranslationsDictionary } from '@/modules/i18n/locales.types';
export const translations: Partial<TranslationsDictionary> = {
// Authentication
'auth.request-password-reset.title': 'Reimposta la tua password',
'auth.request-password-reset.description': 'Inserisci la tua email per reimpostare la password.',
'auth.request-password-reset.requested': 'Se esiste un account per questa email, ti abbiamo inviato un\'email per reimpostare la password.',
'auth.request-password-reset.back-to-login': 'Torna al login',
'auth.request-password-reset.form.email.label': 'Email',
'auth.request-password-reset.form.email.placeholder': 'Esempio: ada@papra.app',
'auth.request-password-reset.form.email.required': 'Inserisci il tuo indirizzo email',
'auth.request-password-reset.form.email.invalid': 'Questo indirizzo email non è valido',
'auth.request-password-reset.form.submit': 'Richiedi reimpostazione password',
'auth.reset-password.title': 'Reimposta la tua password',
'auth.reset-password.description': 'Inserisci la nuova password per reimpostare la password.',
'auth.reset-password.reset': 'La tua password è stata reimpostata.',
'auth.reset-password.back-to-login': 'Torna al login',
'auth.reset-password.form.new-password.label': 'Nuova password',
'auth.reset-password.form.new-password.placeholder': 'Esempio: **********',
'auth.reset-password.form.new-password.required': 'Inserisci la tua nuova password',
'auth.reset-password.form.new-password.min-length': 'La password deve essere di almeno {{ minLength }} caratteri',
'auth.reset-password.form.new-password.max-length': 'La password deve essere inferiore a {{ maxLength }} caratteri',
'auth.reset-password.form.submit': 'Reimposta password',
'auth.email-provider.open': 'Apri {{ provider }}',
'auth.login.title': 'Accedi a Papra',
'auth.login.description': 'Inserisci la tua email o usa un provider per accedere al tuo account Papra.',
'auth.login.login-with-provider': 'Accedi con {{ provider }}',
'auth.login.no-account': 'Non hai un account?',
'auth.login.register': 'Registrati',
'auth.login.form.email.label': 'Email',
'auth.login.form.email.placeholder': 'Esempio: ada@papra.app',
'auth.login.form.email.required': 'Inserisci il tuo indirizzo email',
'auth.login.form.email.invalid': 'Questo indirizzo email non è valido',
'auth.login.form.password.label': 'Password',
'auth.login.form.password.placeholder': 'Imposta una password',
'auth.login.form.password.required': 'Inserisci la tua password',
'auth.login.form.remember-me.label': 'Ricordami',
'auth.login.form.forgot-password.label': 'Password dimenticata?',
'auth.login.form.submit': 'Accedi',
'auth.register.title': 'Registrati a Papra',
'auth.register.description': 'Crea un account per iniziare a usare Papra.',
'auth.register.register-with-email': 'Registrati tramite email',
'auth.register.register-with-provider': 'Registrati tramite {{ provider }}',
'auth.register.providers.google': 'Google',
'auth.register.providers.github': 'GitHub',
'auth.register.have-account': 'Hai già un account?',
'auth.register.login': 'Accedi',
'auth.register.registration-disabled.title': 'Registrazione disabilitata',
'auth.register.registration-disabled.description': 'La creazione di nuovi account è attualmente disabilitata su questa istanza di Papra. Solo gli utenti con account esistenti possono accedere. Se pensi che sia un errore, contatta l\'amministratore di questa istanza.',
'auth.register.form.email.label': 'Email',
'auth.register.form.email.placeholder': 'Esempio: ada@papra.app',
'auth.register.form.email.required': 'Inserisci il tuo indirizzo email',
'auth.register.form.email.invalid': 'Questo indirizzo email non è valido',
'auth.register.form.password.label': 'Password',
'auth.register.form.password.placeholder': 'Imposta una password',
'auth.register.form.password.required': 'Inserisci la tua password',
'auth.register.form.password.min-length': 'La password deve essere di almeno {{ minLength }} caratteri',
'auth.register.form.password.max-length': 'La password deve essere inferiore a {{ maxLength }} caratteri',
'auth.register.form.name.label': 'Nome',
'auth.register.form.name.placeholder': 'Esempio: Ada Lovelace',
'auth.register.form.name.required': 'Inserisci il tuo nome',
'auth.register.form.name.max-length': 'Il nome deve essere inferiore a {{ maxLength }} caratteri',
'auth.register.form.submit': 'Registrati',
'auth.email-validation-required.title': 'Verifica la tua email',
'auth.email-validation-required.description': 'Una email di verifica è stata inviata al tuo indirizzo email. Verifica il tuo indirizzo cliccando il link nell\'email.',
'auth.legal-links.description': 'Continuando, confermi di aver letto e accettato i {{ terms }} e l\'{{ privacy }}.',
'auth.legal-links.terms': 'Termini di servizio',
'auth.legal-links.privacy': 'Informativa sulla privacy',
'auth.no-auth-provider.title': 'Nessun provider di autenticazione',
'auth.no-auth-provider.description': 'Nessun provider di autenticazione è abilitato su questa istanza di Papra. Contatta l\'amministratore di questa istanza per abilitarli.',
// User settings
'user.settings.title': 'Impostazioni utente',
'user.settings.description': 'Gestisci qui le impostazioni del tuo account.',
'user.settings.email.title': 'Indirizzo email',
'user.settings.email.description': 'Il tuo indirizzo email non può essere modificato.',
'user.settings.email.label': 'Indirizzo email',
'user.settings.name.title': 'Nome completo',
'user.settings.name.description': 'Il tuo nome completo è visibile agli altri membri dell\'organizzazione.',
'user.settings.name.label': 'Nome completo',
'user.settings.name.placeholder': 'Es. Mario Rossi',
'user.settings.name.update': 'Aggiorna nome',
'user.settings.name.updated': 'Il tuo nome completo è stato aggiornato',
'user.settings.logout.title': 'Logout',
'user.settings.logout.description': 'Esci dal tuo account. Potrai accedere nuovamente in seguito.',
'user.settings.logout.button': 'Esci',
// Organizations
'organizations.list.title': 'Le tue organizzazioni',
'organizations.list.description': 'Le organizzazioni sono un modo per raggruppare i tuoi documenti e gestire l\'accesso. Puoi creare più organizzazioni e invitare i tuoi collaboratori.',
'organizations.list.create-new': 'Crea una nuova organizzazione',
'organizations.list.back': 'Torna alle organizzazioni',
'organizations.list.deleted.title': 'Organizzazioni eliminate',
'organizations.list.deleted.description': 'Le organizzazioni eliminate vengono conservate per {{ days }} giorni prima di essere rimosse definitivamente. Puoi ripristinarle durante questo periodo.',
'organizations.list.deleted.empty': 'Nessuna organizzazione eliminata',
'organizations.list.deleted.empty-description': 'Quando elimini un\'organizzazione, apparirà qui per {{ days }} giorni prima di essere eliminata definitivamente.',
'organizations.list.deleted.restore': 'Ripristina',
'organizations.list.deleted.restore-success': 'Organizzazione ripristinata con successo',
'organizations.list.deleted.restore-confirm.title': 'Ripristina organizzazione',
'organizations.list.deleted.restore-confirm.message': 'Sei sicuro di voler ripristinare questa organizzazione? Verrà rimossa nella tua lista di organizzazioni attive.',
'organizations.list.deleted.restore-confirm.confirm-button': 'Ripristina organizzazione',
'organizations.list.deleted.deleted-at': 'Eliminata il {{ date }}',
'organizations.list.deleted.purge-at': 'Sarà eliminata definitivamente il {{ date }}',
'organizations.list.deleted.days-remaining': '({{ daysUntilPurge, =1:{daysUntilPurge} giorno, {daysUntilPurge} giorni }} rimanent{{ daysUntilPurge, =1:e, i}})',
'organizations.details.no-documents.title': 'Nessun documento',
'organizations.details.no-documents.description': 'Non ci sono ancora documenti in questa organizzazione. Inizia caricando dei documenti.',
'organizations.details.upload-documents': 'Carica documenti',
'organizations.details.documents-count': 'documenti in totale',
'organizations.details.total-size': 'dimensione totale',
'organizations.details.latest-documents': 'Ultimi documenti importati',
'organizations.create.title': 'Crea una nuova organizzazione',
'organizations.create.description': 'I tuoi documenti saranno raggruppati per organizzazione. Puoi creare più organizzazioni per separare i documenti, ad esempio per uso personale e lavorativo.',
'organizations.create.back': 'Indietro',
'organizations.create.error.max-count-reached': 'Hai raggiunto il numero massimo di organizzazioni che puoi creare, se hai bisogno di crearne altre contatta il supporto.',
'organizations.create.form.name.label': 'Nome organizzazione',
'organizations.create.form.name.placeholder': 'Es. Acme Inc.',
'organizations.create.form.name.required': 'Inserisci il nome dell\'organizzazione',
'organizations.create.form.submit': 'Crea organizzazione',
'organizations.create.success': 'Organizzazione creata con successo',
'organizations.create-first.title': 'Crea la tua organizzazione',
'organizations.create-first.description': 'I tuoi documenti saranno raggruppati per organizzazione. Puoi creare più organizzazioni per separare i documenti, ad esempio per uso personale e lavorativo.',
'organizations.create-first.default-name': 'La mia organizzazione',
'organizations.create-first.user-name': 'Organizzazione di {{ name }}',
'organization.settings.title': 'Impostazioni organizzazione',
'organization.settings.page.title': 'Impostazioni organizzazione',
'organization.settings.page.description': 'Gestisci qui le impostazioni della tua organizzazione.',
'organization.settings.name.title': 'Nome organizzazione',
'organization.settings.name.update': 'Aggiorna nome',
'organization.settings.name.placeholder': 'Es. Acme Inc.',
'organization.settings.name.updated': 'Nome organizzazione aggiornato',
'organization.settings.subscription.title': 'Sottoscrizione',
'organization.settings.subscription.description': 'Gestisci fatturazione, fatture e metodi di pagamento.',
'organization.settings.subscription.manage': 'Gestisci sottoscrizione',
'organization.settings.subscription.error': 'Impossibile ottenere l\'URL del portale clienti',
'organization.settings.delete.title': 'Elimina organizzazione',
'organization.settings.delete.description': 'Eliminando questa organizzazione rimuoverai definitivamente tutti i dati associati.',
'organization.settings.delete.confirm.title': 'Elimina organizzazione',
'organization.settings.delete.confirm.message': 'Sei sicuro di voler eliminare questa organizzazione? L\'organizzazione verrà contrassegnata per l\'eliminazione e rimossa definitivamente dopo {{ days }} giorni. Durante questo periodo, puoi ripristinarla dalla tua lista di organizzazioni. Tutti i documenti e i dati verranno eliminati definitivamente dopo questo periodo.',
'organization.settings.delete.confirm.confirm-button': 'Elimina organizzazione',
'organization.settings.delete.confirm.cancel-button': 'Annulla',
'organization.settings.delete.success': 'Organizzazione eliminata',
'organization.settings.delete.only-owner': 'Solo il proprietario dell\'organizzazione può eliminare questa organizzazione.',
'organization.usage.page.title': 'Utilizzo',
'organization.usage.page.description': 'Visualizza l\'utilizzo attuale e i limiti della tua organizzazione.',
'organization.usage.storage.title': 'Archiviazione documenti',
'organization.usage.storage.description': 'Archiviazione totale utilizzata dai tuoi documenti',
'organization.usage.intake-emails.title': 'Email di acquisizione',
'organization.usage.intake-emails.description': 'Numero di indirizzi email di acquisizione',
'organization.usage.members.title': 'Membri',
'organization.usage.members.description': 'Numero di membri nell\'organizzazione',
'organization.usage.unlimited': 'Illimitato',
'organizations.members.title': 'Membri',
'organizations.members.description': 'Gestisci i membri della tua organizzazione',
'organizations.members.invite-member': 'Invita membro',
'organizations.members.invite-member-disabled-tooltip': 'Solo gli amministratori o i proprietari possono invitare membri nell\'organizzazione',
'organizations.members.remove-from-organization': 'Rimuovi dall\'organizzazione',
'organizations.members.role': 'Ruolo',
'organizations.members.roles.owner': 'Proprietario',
'organizations.members.roles.admin': 'Amministratore',
'organizations.members.roles.member': 'Membro',
'organizations.members.delete.confirm.title': 'Rimuovi membro',
'organizations.members.delete.confirm.message': 'Sei sicuro di voler rimuovere questo membro dall\'organizzazione?',
'organizations.members.delete.confirm.confirm-button': 'Rimuovi',
'organizations.members.delete.confirm.cancel-button': 'Annulla',
'organizations.members.delete.success': 'Membro rimosso dall\'organizzazione',
'organizations.members.update-role.success': 'Ruolo del membro aggiornato',
'organizations.members.table.headers.name': 'Nome',
'organizations.members.table.headers.email': 'Email',
'organizations.members.table.headers.role': 'Ruolo',
'organizations.members.table.headers.created': 'Creato',
'organizations.members.table.headers.actions': 'Azioni',
'organizations.invite-member.title': 'Invita membro',
'organizations.invite-member.description': 'Invita un membro nella tua organizzazione',
'organizations.invite-member.form.email.label': 'Email',
'organizations.invite-member.form.email.placeholder': 'Esempio: ada@papra.app',
'organizations.invite-member.form.email.required': 'Inserisci un indirizzo email valido',
'organizations.invite-member.form.role.label': 'Ruolo',
'organizations.invite-member.form.submit': 'Invita nell\'organizzazione',
'organizations.invite-member.success.message': 'Membro invitato',
'organizations.invite-member.success.description': 'Il membro è stato invitato nell\'organizzazione.',
'organizations.invite-member.error.message': 'Impossibile invitare il membro',
'organizations.invitations.title': 'Inviti',
'organizations.invitations.description': 'Gestisci gli inviti della tua organizzazione',
'organizations.invitations.list.cta': 'Invita membro',
'organizations.invitations.list.empty.title': 'Nessun invito in sospeso',
'organizations.invitations.list.empty.description': 'Non sei stato ancora invitato in nessuna organizzazione.',
'organizations.invitations.status.pending': 'In sospeso',
'organizations.invitations.status.accepted': 'Accettato',
'organizations.invitations.status.rejected': 'Rifiutato',
'organizations.invitations.status.expired': 'Scaduto',
'organizations.invitations.status.cancelled': 'Cancellato',
'organizations.invitations.resend': 'Invia di nuovo invito',
'organizations.invitations.cancel.title': 'Annulla invito',
'organizations.invitations.cancel.description': 'Sei sicuro di voler annullare questo invito?',
'organizations.invitations.cancel.confirm': 'Annulla invito',
'organizations.invitations.cancel.cancel': 'Annulla',
'organizations.invitations.resend.title': 'Invia di nuovo invito',
'organizations.invitations.resend.description': 'Sei sicuro di voler inviare nuovamente questo invito? Sarà inviata una nuova email al destinatario.',
'organizations.invitations.resend.confirm': 'Invia invito',
'organizations.invitations.resend.cancel': 'Annulla',
'invitations.list.title': 'Inviti',
'invitations.list.description': 'Gestisci gli inviti della tua organizzazione',
'invitations.list.empty.title': 'Nessun invito in sospeso',
'invitations.list.empty.description': 'Non sei stato ancora invitato in nessuna organizzazione.',
'invitations.list.headers.organization': 'Organizzazione',
'invitations.list.headers.status': 'Stato',
'invitations.list.headers.created': 'Creato',
'invitations.list.headers.actions': 'Azioni',
'invitations.list.actions.accept': 'Accetta',
'invitations.list.actions.reject': 'Rifiuta',
'invitations.list.actions.accept.success.message': 'Invito accettato',
'invitations.list.actions.accept.success.description': 'L\'invito è stato accettato.',
'invitations.list.actions.reject.success.message': 'Invito rifiutato',
'invitations.list.actions.reject.success.description': 'L\'invito è stato rifiutato.',
// Documents
'documents.list.title': 'Documenti',
'documents.list.no-documents.title': 'Nessun documento',
'documents.list.no-documents.description': 'Non ci sono ancora documenti in questa organizzazione. Inizia caricando dei documenti.',
'documents.list.no-results': 'Nessun documento trovato',
'documents.tabs.info': 'Info',
'documents.tabs.content': 'Contenuto',
'documents.tabs.activity': 'Attività',
'documents.deleted.message': 'Questo documento è stato eliminato e sarà rimosso definitivamente tra {{ days }} giorni.',
'documents.actions.download': 'Scarica',
'documents.actions.open-in-new-tab': 'Apri in una nuova scheda',
'documents.actions.restore': 'Ripristina',
'documents.actions.delete': 'Elimina',
'documents.actions.edit': 'Modifica',
'documents.actions.cancel': 'Annulla',
'documents.actions.save': 'Salva',
'documents.actions.saving': 'Salvataggio in corso...',
'documents.content.alert': 'Il contenuto del documento è estratto automaticamente al caricamento. È usato solo per la ricerca e l\'indicizzazione.',
'documents.content.empty-placeholder': 'Questo documento non ha contenuto estratto, puoi inserirlo manualmente qui.',
'documents.info.id': 'ID',
'documents.info.name': 'Nome',
'documents.info.type': 'Tipo',
'documents.info.size': 'Dimensione',
'documents.info.created-at': 'Creato il',
'documents.info.updated-at': 'Aggiornato il',
'documents.info.never': 'Mai',
'documents.rename.title': 'Rinomina documento',
'documents.rename.form.name.label': 'Nome',
'documents.rename.form.name.placeholder': 'Esempio: Fattura 2024',
'documents.rename.form.name.required': 'Inserisci un nome per il documento',
'documents.rename.form.name.max-length': 'Il nome deve essere inferiore a 255 caratteri',
'documents.rename.form.submit': 'Rinomina documento',
'documents.rename.success': 'Documento rinominato con successo',
'documents.rename.cancel': 'Annulla',
'import-documents.title.error': '{{ count }} documenti non importati',
'import-documents.title.success': '{{ count }} documenti importati',
'import-documents.title.pending': '{{ count }} / {{ total }} documenti importati',
'import-documents.title.none': 'Importa documenti',
'import-documents.no-import-in-progress': 'Nessuna importazione documenti in corso',
'documents.deleted.title': 'Documenti eliminati',
'documents.deleted.empty.title': 'Nessun documento eliminato',
'documents.deleted.empty.description': 'Non hai documenti eliminati. I documenti eliminati saranno spostati nel cestino per {{ days }} giorni.',
'documents.deleted.retention-notice': 'Tutti i documenti eliminati sono conservati nel cestino per {{ days }} giorni. Passato questo periodo, saranno eliminati definitivamente e non potrai recuperarli.',
'documents.deleted.deleted-at': 'Eliminato il',
'documents.deleted.restoring': 'Ripristino in corso...',
'documents.deleted.deleting': 'Eliminazione in corso...',
'documents.preview.unknown-file-type': 'Nessuna anteprima disponibile per questo tipo di file',
'documents.preview.binary-file': 'Sembra essere un file binario e non può essere visualizzato come testo',
'trash.delete-all.button': 'Elimina tutto',
'trash.delete-all.confirm.title': 'Eliminare definitivamente tutti i documenti?',
'trash.delete-all.confirm.description': 'Sei sicuro di voler eliminare definitivamente tutti i documenti dal cestino? Questa azione non può essere annullata.',
'trash.delete-all.confirm.label': 'Elimina',
'trash.delete-all.confirm.cancel': 'Annulla',
'trash.delete.button': 'Elimina',
'trash.delete.confirm.title': 'Eliminare definitivamente il documento?',
'trash.delete.confirm.description': 'Sei sicuro di voler eliminare definitivamente questo documento dal cestino? Questa azione non può essere annullata.',
'trash.delete.confirm.label': 'Elimina',
'trash.delete.confirm.cancel': 'Annulla',
'trash.deleted.success.title': 'Documento eliminato',
'trash.deleted.success.description': 'Il documento è stato eliminato definitivamente.',
'activity.document.created': 'Documento creato',
'activity.document.updated.single': 'Il campo {{ field }} è stato aggiornato',
'activity.document.updated.multiple': 'I campi {{ fields }} sono stati aggiornati',
'activity.document.updated': 'Documento aggiornato',
'activity.document.deleted': 'Documento eliminato',
'activity.document.restored': 'Documento ripristinato',
'activity.document.tagged': 'Tag {{ tag }} aggiunto',
'activity.document.untagged': 'Tag {{ tag }} rimosso',
'activity.document.user.name': 'da {{ name }}',
'activity.load-more': 'Carica altri',
'activity.no-more-activities': 'Nessuna altra attività per questo documento',
// Tags
'tags.no-tags.title': 'Nessun tag',
'tags.no-tags.description': 'Questa organizzazione non ha ancora tag. I tag vengono usati per categorizzare i documenti. Puoi aggiungere tag ai tuoi documenti per trovarli e organizzarli più facilmente.',
'tags.no-tags.create-tag': 'Crea tag',
'tags.title': 'Tag dei documenti',
'tags.description': 'I tag vengono usati per categorizzare i documenti. Puoi aggiungere tag ai tuoi documenti per trovarli e organizzarli più facilmente.',
'tags.create': 'Crea tag',
'tags.update': 'Aggiorna tag',
'tags.delete': 'Elimina tag',
'tags.delete.confirm.title': 'Elimina tag',
'tags.delete.confirm.message': 'Sei sicuro di voler eliminare questo tag? Il tag verrà rimosso da tutti i documenti.',
'tags.delete.confirm.confirm-button': 'Elimina',
'tags.delete.confirm.cancel-button': 'Annulla',
'tags.delete.success': 'Tag eliminato con successo',
'tags.create.success': 'Tag "{{ name }}" creato con successo.',
'tags.update.success': 'Tag "{{ name }}" aggiornato con successo.',
'tags.form.name.label': 'Nome',
'tags.form.name.placeholder': 'Es. Contratti',
'tags.form.name.required': 'Inserisci un nome per il tag',
'tags.form.name.max-length': 'Il nome del tag deve essere inferiore a 64 caratteri',
'tags.form.color.label': 'Colore',
'tags.form.color.required': 'Inserisci un colore',
'tags.form.color.invalid': 'Il colore hex non è formattato correttamente.',
'tags.form.description.label': 'Descrizione',
'tags.form.description.optional': '(opzionale)',
'tags.form.description.placeholder': 'Es. Tutti i contratti firmati dall\'azienda',
'tags.form.description.max-length': 'La descrizione deve essere inferiore a 256 caratteri',
'tags.form.no-description': 'Nessuna descrizione',
'tags.table.headers.tag': 'Tag',
'tags.table.headers.description': 'Descrizione',
'tags.table.headers.documents': 'Documenti',
'tags.table.headers.created': 'Creato',
'tags.table.headers.actions': 'Azioni',
// Tagging rules
'tagging-rules.field.name': 'nome documento',
'tagging-rules.field.content': 'contenuto documento',
'tagging-rules.operator.equals': 'uguale a',
'tagging-rules.operator.not-equals': 'diverso da',
'tagging-rules.operator.contains': 'contiene',
'tagging-rules.operator.not-contains': 'non contiene',
'tagging-rules.operator.starts-with': 'inizia con',
'tagging-rules.operator.ends-with': 'termina con',
'tagging-rules.list.title': 'Regole di tagging',
'tagging-rules.list.description': 'Gestisci le regole di tagging della tua organizzazione per taggare automaticamente i documenti in base a condizioni definite da te.',
'tagging-rules.list.demo-warning': 'Nota: Essendo un ambiente demo (senza server), le regole di tagging non verranno applicate ai nuovi documenti.',
'tagging-rules.list.no-tagging-rules.title': 'Nessuna regola di tagging',
'tagging-rules.list.no-tagging-rules.description': 'Crea una regola per taggare automaticamente i documenti aggiunti in base a condizioni definite da te.',
'tagging-rules.list.no-tagging-rules.create-tagging-rule': 'Crea regola di tagging',
'tagging-rules.list.card.no-conditions': 'Nessuna condizione',
'tagging-rules.list.card.one-condition': '1 condizione',
'tagging-rules.list.card.conditions': '{{ count }} condizioni',
'tagging-rules.list.card.delete': 'Elimina regola',
'tagging-rules.list.card.edit': 'Modifica regola',
'tagging-rules.create.title': 'Crea regola di tagging',
'tagging-rules.create.success': 'Regola di tagging creata con successo',
'tagging-rules.create.error': 'Errore nella creazione della regola di tagging',
'tagging-rules.create.submit': 'Crea regola',
'tagging-rules.form.name.label': 'Nome',
'tagging-rules.form.name.placeholder': 'Esempio: Tagga fatture',
'tagging-rules.form.name.min-length': 'Inserisci un nome per la regola',
'tagging-rules.form.name.max-length': 'Il nome deve essere inferiore a 64 caratteri',
'tagging-rules.form.description.label': 'Descrizione',
'tagging-rules.form.description.placeholder': 'Esempio: Tagga i documenti con \'fattura\' nel nome',
'tagging-rules.form.description.max-length': 'La descrizione deve essere inferiore a 256 caratteri',
'tagging-rules.form.conditions.label': 'Condizioni',
'tagging-rules.form.conditions.description': 'Definisci le condizioni che devono essere soddisfatte affinché la regola si applichi. Tutte le condizioni devono essere soddisfatte.',
'tagging-rules.form.conditions.add-condition': 'Aggiungi condizione',
'tagging-rules.form.conditions.no-conditions.title': 'Nessuna condizione',
'tagging-rules.form.conditions.no-conditions.description': 'Non hai aggiunto nessuna condizione a questa regola. Questa regola applicherà i suoi tag a tutti i documenti.',
'tagging-rules.form.conditions.no-conditions.confirm': 'Applica regola senza condizioni',
'tagging-rules.form.conditions.no-conditions.cancel': 'Annulla',
'tagging-rules.form.conditions.value.placeholder': 'Esempio: fattura',
'tagging-rules.form.conditions.value.min-length': 'Inserisci un valore per la condizione',
'tagging-rules.form.tags.label': 'Tag',
'tagging-rules.form.tags.description': 'Seleziona i tag da applicare ai documenti che soddisfano le condizioni',
'tagging-rules.form.tags.min-length': 'È richiesto almeno un tag da applicare',
'tagging-rules.form.tags.add-tag': 'Crea tag',
'tagging-rules.form.submit': 'Crea regola',
'tagging-rules.update.title': 'Aggiorna regola di tagging',
'tagging-rules.update.error': 'Errore nell\'aggiornamento della regola di tagging',
'tagging-rules.update.submit': 'Aggiorna regola',
'tagging-rules.update.cancel': 'Annulla',
// Intake emails
'intake-emails.title': 'Email di acquisizione',
'intake-emails.description': 'Gli indirizzi email di acquisizione vengono usati per importare automaticamente email in Papra. Basta inoltrare le email all\'indirizzo di acquisizione e gli allegati saranno aggiunti ai documenti dell\'organizzazione.',
'intake-emails.disabled.title': 'Email di acquisizione disabilitate',
'intake-emails.disabled.description': 'Le email di acquisizione sono disabilitate su questa istanza. Contatta il tuo amministratore per abilitarle. Consulta la {{ documentation }} per maggiori informazioni.',
'intake-emails.disabled.documentation': 'documentazione',
'intake-emails.info': 'Solo le email di acquisizione abilitate provenienti da origini consentite saranno processate. Puoi abilitare o disabilitare un\'email di acquisizione in qualsiasi momento.',
'intake-emails.empty.title': 'Nessuna email di acquisizione',
'intake-emails.empty.description': 'Genera un indirizzo di acquisizione per importare facilmente allegati email.',
'intake-emails.empty.generate': 'Genera email di acquisizione',
'intake-emails.count': '{{ count }} email di acquisizione per questa organizzazione',
'intake-emails.new': 'Nuova email di acquisizione',
'intake-emails.disabled-label': '(Disabilitata)',
'intake-emails.no-origins': 'Nessuna origine email consentita',
'intake-emails.allowed-origins': 'Consentito da {{ count }} indirizzo/i',
'intake-emails.actions.enable': 'Abilita',
'intake-emails.actions.disable': 'Disabilita',
'intake-emails.actions.manage-origins': 'Gestisci indirizzi origine',
'intake-emails.actions.delete': 'Elimina',
'intake-emails.delete.confirm.title': 'Eliminare l\'email di acquisizione?',
'intake-emails.delete.confirm.message': 'Sei sicuro di voler eliminare questa email di acquisizione? Questa azione non può essere annullata.',
'intake-emails.delete.confirm.confirm-button': 'Elimina email di acquisizione',
'intake-emails.delete.confirm.cancel-button': 'Annulla',
'intake-emails.delete.success': 'Email di acquisizione eliminata',
'intake-emails.create.success': 'Email di acquisizione creata',
'intake-emails.update.success.enabled': 'Email di acquisizione abilitata',
'intake-emails.update.success.disabled': 'Email di acquisizione disabilitata',
'intake-emails.allowed-origins.title': 'Origini consentite',
'intake-emails.allowed-origins.description': 'Solo le email inviate a {{ email }} da queste origini saranno processate. Se non sono specificate origini, tutte le email saranno scartate.',
'intake-emails.allowed-origins.add.label': 'Aggiungi email origine consentita',
'intake-emails.allowed-origins.add.placeholder': 'Es. ada@papra.app',
'intake-emails.allowed-origins.add.button': 'Aggiungi',
'intake-emails.allowed-origins.add.error.exists': 'Questa email è già tra le origini consentite per questa email di acquisizione',
// API keys
'api-keys.permissions.select-all': 'Seleziona tutto',
'api-keys.permissions.deselect-all': 'Deseleziona tutto',
'api-keys.permissions.organizations.title': 'Organizzazioni',
'api-keys.permissions.organizations.organizations:create': 'Crea organizzazioni',
'api-keys.permissions.organizations.organizations:read': 'Leggi organizzazioni',
'api-keys.permissions.organizations.organizations:update': 'Aggiorna organizzazioni',
'api-keys.permissions.organizations.organizations:delete': 'Elimina organizzazioni',
'api-keys.permissions.documents.title': 'Documenti',
'api-keys.permissions.documents.documents:create': 'Crea documenti',
'api-keys.permissions.documents.documents:read': 'Leggi documenti',
'api-keys.permissions.documents.documents:update': 'Aggiorna documenti',
'api-keys.permissions.documents.documents:delete': 'Elimina documenti',
'api-keys.permissions.tags.title': 'Tag',
'api-keys.permissions.tags.tags:create': 'Crea tag',
'api-keys.permissions.tags.tags:read': 'Leggi tag',
'api-keys.permissions.tags.tags:update': 'Aggiorna tag',
'api-keys.permissions.tags.tags:delete': 'Elimina tag',
'api-keys.create.title': 'Crea chiave API',
'api-keys.create.description': 'Crea una nuova chiave API per accedere all\'API di Papra.',
'api-keys.create.success': 'La chiave API è stata creata con successo.',
'api-keys.create.back': 'Torna alle chiavi API',
'api-keys.create.form.name.label': 'Nome',
'api-keys.create.form.name.placeholder': 'Esempio: La mia chiave API',
'api-keys.create.form.name.required': 'Inserisci un nome per la chiave API',
'api-keys.create.form.permissions.label': 'Permessi',
'api-keys.create.form.permissions.required': 'Seleziona almeno un permesso',
'api-keys.create.form.submit': 'Crea chiave API',
'api-keys.create.created.title': 'Chiave API creata',
'api-keys.create.created.description': 'La chiave API è stata creata con successo. Salvala in un luogo sicuro, non verrà più mostrata.',
'api-keys.list.title': 'Chiavi API',
'api-keys.list.description': 'Gestisci qui le tue chiavi API.',
'api-keys.list.create': 'Crea chiave API',
'api-keys.list.empty.title': 'Nessuna chiave API',
'api-keys.list.empty.description': 'Crea una chiave API per accedere all\'API di Papra.',
'api-keys.list.card.last-used': 'Ultimo utilizzo',
'api-keys.list.card.never': 'Mai',
'api-keys.list.card.created': 'Creato',
'api-keys.delete.success': 'La chiave API è stata eliminata con successo',
'api-keys.delete.confirm.title': 'Eliminare la chiave API',
'api-keys.delete.confirm.message': 'Sei sicuro di voler eliminare questa chiave API? Questa azione non può essere annullata.',
'api-keys.delete.confirm.confirm-button': 'Elimina',
'api-keys.delete.confirm.cancel-button': 'Annulla',
// Webhooks
'webhooks.list.title': 'Webhook',
'webhooks.list.description': 'Gestisci i webhook della tua organizzazione',
'webhooks.list.empty.title': 'Nessun webhook',
'webhooks.list.empty.description': 'Crea il tuo primo webhook per iniziare a ricevere eventi',
'webhooks.list.create': 'Crea webhook',
'webhooks.list.card.last-triggered': 'Ultima attivazione',
'webhooks.list.card.never': 'Mai',
'webhooks.list.card.created': 'Creato',
'webhooks.create.title': 'Crea webhook',
'webhooks.create.description': 'Crea un nuovo webhook per ricevere eventi',
'webhooks.create.success': 'Webhook creato con successo',
'webhooks.create.back': 'Indietro',
'webhooks.create.form.submit': 'Crea webhook',
'webhooks.create.form.name.label': 'Nome webhook',
'webhooks.create.form.name.placeholder': 'Inserisci nome webhook',
'webhooks.create.form.name.required': 'Il nome è obbligatorio',
'webhooks.create.form.url.label': 'URL webhook',
'webhooks.create.form.url.placeholder': 'Inserisci URL webhook',
'webhooks.create.form.url.required': 'L\'URL è obbligatorio',
'webhooks.create.form.url.invalid': 'L\'URL non è valido',
'webhooks.create.form.secret.label': 'Segreto',
'webhooks.create.form.secret.placeholder': 'Inserisci il segreto del webhook',
'webhooks.create.form.events.label': 'Eventi',
'webhooks.create.form.events.required': 'È richiesto almeno un evento',
'webhooks.update.title': 'Modifica webhook',
'webhooks.update.description': 'Aggiorna i dettagli del webhook',
'webhooks.update.success': 'Webhook aggiornato con successo',
'webhooks.update.submit': 'Aggiorna webhook',
'webhooks.update.cancel': 'Annulla',
'webhooks.update.form.secret.placeholder': 'Inserisci nuovo segreto',
'webhooks.update.form.secret.placeholder-redacted': '[Segreto nascosto]',
'webhooks.update.form.rotate-secret.button': 'Rigenera segreto',
'webhooks.delete.success': 'Webhook eliminato con successo',
'webhooks.delete.confirm.title': 'Eliminare webhook',
'webhooks.delete.confirm.message': 'Sei sicuro di voler eliminare questo webhook?',
'webhooks.delete.confirm.confirm-button': 'Elimina',
'webhooks.delete.confirm.cancel-button': 'Annulla',
'webhooks.events.documents.title': 'Eventi documenti',
'webhooks.events.documents.document:created.description': 'Documento creato',
'webhooks.events.documents.document:deleted.description': 'Documento eliminato',
'webhooks.events.documents.document:updated.description': 'Documento aggiornato',
'webhooks.events.documents.document:tag:added.description': 'Un tag è stato aggiunto a un documento',
'webhooks.events.documents.document:tag:removed.description': 'Un tag è stato rimosso da un documento',
// Navigation
'layout.menu.home': 'Home',
'layout.menu.documents': 'Documenti',
'layout.menu.tags': 'Tag',
'layout.menu.tagging-rules': 'Regole di tagging',
'layout.menu.deleted-documents': 'Documenti eliminati',
'layout.menu.organization-settings': 'Impostazioni',
'layout.menu.api-keys': 'Chiavi API',
'layout.menu.settings': 'Impostazioni',
'layout.menu.account': 'Account',
'layout.menu.general-settings': 'Impostazioni generali',
'layout.menu.usage': 'Utilizzo',
'layout.menu.intake-emails': 'Email di acquisizione',
'layout.menu.webhooks': 'Webhook',
'layout.menu.members': 'Membri',
'layout.menu.invitations': 'Inviti',
'layout.upgrade-cta.title': 'Serve più spazio?',
'layout.upgrade-cta.description': 'Ottieni 10x più storage + collaborazione del team',
'layout.upgrade-cta.button': 'Aggiorna ora',
'layout.theme.light': 'Modalità chiara',
'layout.theme.dark': 'Modalità scura',
'layout.theme.system': 'Modalità sistema',
'layout.search.placeholder': 'Cerca...',
'layout.menu.import-document': 'Importa un documento',
'user-menu.account-settings': 'Impostazioni account',
'user-menu.api-keys': 'Chiavi API',
'user-menu.invitations': 'Inviti',
'user-menu.language': 'Lingua',
'user-menu.logout': 'Esci',
// Command palette
'command-palette.search.placeholder': 'Cerca comandi o documenti',
'command-palette.no-results': 'Nessun risultato trovato',
'command-palette.sections.documents': 'Documenti',
'command-palette.sections.theme': 'Tema',
// API errors
'api-errors.document.already_exists': 'Il documento esiste già',
'api-errors.document.size_too_large': 'Il file è troppo grande',
'api-errors.intake-emails.already_exists': 'Un\'email di acquisizione con questo indirizzo esiste già.',
'api-errors.intake_email.limit_reached': 'È stato raggiunto il numero massimo di email di acquisizione per questa organizzazione. Aggiorna il tuo piano per crearne altre.',
'api-errors.user.max_organization_count_reached': 'Hai raggiunto il numero massimo di organizzazioni che puoi creare, se hai bisogno di crearne altre contatta il supporto.',
'api-errors.default': 'Si è verificato un errore durante l\'elaborazione della richiesta.',
'api-errors.organization.invitation_already_exists': 'Esiste già un invito per questa email in questa organizzazione.',
'api-errors.user.already_in_organization': 'Questo utente è già in questa organizzazione.',
'api-errors.user.organization_invitation_limit_reached': 'È stato raggiunto il numero massimo di inviti per oggi. Riprova domani.',
'api-errors.demo.not_available': 'Questa funzionalità non è disponibile nella demo',
'api-errors.tags.already_exists': 'Esiste già un tag con questo nome per questa organizzazione',
'api-errors.internal.error': 'Si è verificato un errore durante l\'elaborazione della richiesta. Riprova.',
'api-errors.auth.invalid_origin': 'Origine dell\'applicazione non valida. Se stai ospitando Papra, assicurati che la variabile di ambiente APP_BASE_URL corrisponda all\'URL corrente. Per maggiori dettagli, consulta https://docs.papra.app/resources/troubleshooting/#invalid-application-origin',
'api-errors.organization.max_members_count_reached': 'È stato raggiunto il numero massimo di membri e inviti in sospeso per questa organizzazione. Aggiorna il tuo piano per aggiungere altri membri.',
// Not found
'not-found.title': '404 - Non trovato',
'not-found.description': 'Spiacenti, la pagina che stai cercando non sembra esistere. Controlla l\'URL e riprova.',
'not-found.back-to-home': 'Torna alla home',
// Demo
'demo.popup.description': 'Questo è un ambiente demo, tutti i dati vengono salvati nello storage locale del browser.',
'demo.popup.discord': 'Unisciti a {{ discordLink }} per ricevere supporto, proporre funzionalità o semplicemente fare due chiacchiere.',
'demo.popup.discord-link-label': 'Server Discord',
'demo.popup.reset': 'Reimposta dati demo',
'demo.popup.hide': 'Nascondi',
// Color picker
'color-picker.hue': 'Tonalità',
'color-picker.saturation': 'Saturazione',
'color-picker.lightness': 'Luminosità',
'color-picker.select-color': 'Seleziona colore',
'color-picker.select-a-color': 'Seleziona un colore',
// Subscriptions
'subscriptions.checkout-success.title': 'Pagamento riuscito!',
'subscriptions.checkout-success.description': 'Il tuo abbonamento è stato attivato con successo.',
'subscriptions.checkout-success.thank-you': 'Grazie per l\'upgrade a Papra Plus. Ora hai accesso a tutte le funzionalità premium.',
'subscriptions.checkout-success.go-to-organizations': 'Vai alle Organizzazioni',
'subscriptions.checkout-success.redirecting': 'Reindirizzamento tra {{ count }} secondo{{ plural }}...',
'subscriptions.checkout-cancel.title': 'Pagamento annullato',
'subscriptions.checkout-cancel.description': 'L\'upgrade del tuo abbonamento è stato annullato.',
'subscriptions.checkout-cancel.no-charges': 'Non sono stati effettuati addebiti sul tuo account. Puoi riprovare quando sei pronto.',
'subscriptions.checkout-cancel.back-to-organizations': 'Torna alle Organizzazioni',
'subscriptions.checkout-cancel.need-help': 'Hai bisogno di aiuto?',
'subscriptions.checkout-cancel.contact-support': 'Contatta il supporto',
'subscriptions.upgrade-dialog.title': 'Aggiorna questa organizzazione',
'subscriptions.upgrade-dialog.description': 'Sblocca funzionalità potenti per la tua organizzazione',
'subscriptions.upgrade-dialog.contact-us': 'Contattaci',
'subscriptions.upgrade-dialog.enterprise-plans': 'se hai bisogno di piani aziendali personalizzati.',
'subscriptions.upgrade-dialog.current-plan': 'Piano attuale',
'subscriptions.upgrade-dialog.recommended': 'Consigliato',
'subscriptions.upgrade-dialog.per-month': '/mese',
'subscriptions.upgrade-dialog.billed-annually': '${{ price }} fatturato annualmente',
'subscriptions.upgrade-dialog.upgrade-now': 'Aggiorna ora',
'subscriptions.plan.free.name': 'Piano gratuito',
'subscriptions.plan.plus.name': 'Plus',
'subscriptions.plan.pro.name': 'Pro',
'subscriptions.features.storage-size': 'Dimensione archiviazione documenti',
'subscriptions.features.members': 'Membri dell\'organizzazione',
'subscriptions.features.members-count': '{{ count }} membri',
'subscriptions.features.email-intakes': 'Email di acquisizione',
'subscriptions.features.email-intakes-count-singular': '{{ count }} indirizzo',
'subscriptions.features.email-intakes-count-plural': '{{ count }} indirizzi',
'subscriptions.features.max-upload-size': 'Dimensione massima file caricamento',
'subscriptions.features.support': 'Supporto',
'subscriptions.features.support-community': 'Supporto della comunità',
'subscriptions.features.support-email': 'Supporto via email',
'subscriptions.features.support-priority': 'Supporto prioritario',
'subscriptions.billing-interval.monthly': 'Mensile',
'subscriptions.billing-interval.annual': 'Annuale',
'subscriptions.usage-warning.message': 'Hai utilizzato il {{ percent }}% dello spazio di archiviazione dei documenti. Considera l\'aggiornamento del piano per ottenere più spazio.',
'subscriptions.usage-warning.upgrade-button': 'Aggiorna piano',
// Common / Shared
'common.confirm-modal.type-to-confirm': 'Digita "{{ text }}" per confermare',
};

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