mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-17 12:15:22 -06:00
Compare commits
123 Commits
v0.3.0
...
@papra/web
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce21981a9 | ||
|
|
3401cfbfdc | ||
|
|
26015666de | ||
|
|
09e3bc5e15 | ||
|
|
1711ef866d | ||
|
|
1d23f40894 | ||
|
|
40a1f91b67 | ||
|
|
47b69b15f4 | ||
|
|
a188af1f88 | ||
|
|
f28d8245bf | ||
|
|
aad36f3252 | ||
|
|
21a5ccce6d | ||
|
|
42bc3c6698 | ||
|
|
a9f474dc2d | ||
|
|
ed5a93cb47 | ||
|
|
52df988c02 | ||
|
|
73b8d08076 | ||
|
|
9b2a4b2ae9 | ||
|
|
2a8b88e48a | ||
|
|
be1b70a26a | ||
|
|
1755849483 | ||
|
|
b3693fd9c9 | ||
|
|
2149b50270 | ||
|
|
0b276ee0d5 | ||
|
|
56fb9ec2c4 | ||
|
|
6cedc30716 | ||
|
|
f1e1b4037b | ||
|
|
205c6cfd46 | ||
|
|
c54a71d2c5 | ||
|
|
62b7f0382c | ||
|
|
57c6a26657 | ||
|
|
b8c2bd70e3 | ||
|
|
0c2cf698d1 | ||
|
|
585c53cd9d | ||
|
|
f035458e16 | ||
|
|
556fd8b167 | ||
|
|
81e85295ba | ||
|
|
1c574b8305 | ||
|
|
ff830c234a | ||
|
|
451564f354 | ||
|
|
ecd6af45c8 | ||
|
|
cb652c7166 | ||
|
|
17ca8f8f81 | ||
|
|
f54b8e162a | ||
|
|
6b435bba79 | ||
|
|
8ccdb74834 | ||
|
|
60059c895c | ||
|
|
6e22a93dff | ||
|
|
79c1d3206b | ||
|
|
48a953a584 | ||
|
|
fdb90fa164 | ||
|
|
e9a205c0a3 | ||
|
|
278db63fc8 | ||
|
|
e5ef40f36c | ||
|
|
27c9e39422 | ||
|
|
91d2e236d0 | ||
|
|
d4f72e889a | ||
|
|
759a3ff713 | ||
|
|
34862991fb | ||
|
|
f0876fdc63 | ||
|
|
cb38d66485 | ||
|
|
c28af1407f | ||
|
|
b62ddf2bc4 | ||
|
|
fa7909c62d | ||
|
|
1996b51b4d | ||
|
|
734027f00c | ||
|
|
557cde940c | ||
|
|
26a83052bd | ||
|
|
5aac3f7ba6 | ||
|
|
0ddc2340f0 | ||
|
|
438a31171c | ||
|
|
53bf93f128 | ||
|
|
b400b3f18d | ||
|
|
0627ec25a4 | ||
|
|
72e5a9a4de | ||
|
|
268ac8e358 | ||
|
|
249b3bcfd2 | ||
|
|
d7838b5d57 | ||
|
|
f170ddd817 | ||
|
|
4f53c70854 | ||
|
|
85fa5c4342 | ||
|
|
c5d984a3a0 | ||
|
|
565bd8d7fd | ||
|
|
9b72aa886c | ||
|
|
7410455093 | ||
|
|
dd8f194fd0 | ||
|
|
803c39cbc8 | ||
|
|
096331a4ee | ||
|
|
59ba9465f6 | ||
|
|
a1056702af | ||
|
|
fd44897bca | ||
|
|
332d836d11 | ||
|
|
f613198cbd | ||
|
|
80491a5a58 | ||
|
|
605e21a410 | ||
|
|
dec589b6ed | ||
|
|
c0bd6e2ae4 | ||
|
|
6287aaa973 | ||
|
|
cc2edc59b0 | ||
|
|
9cba84e38b | ||
|
|
5fe401778d | ||
|
|
38aa1ea7f1 | ||
|
|
ab98c1b255 | ||
|
|
265f06f8b7 | ||
|
|
a787b7915c | ||
|
|
0ba6a09923 | ||
|
|
6880bfd41c | ||
|
|
21a2c95e56 | ||
|
|
19e2083a71 | ||
|
|
e6b2d9fb2d | ||
|
|
f37c7dd8f7 | ||
|
|
a7fbf21a9f | ||
|
|
f97e5f863e | ||
|
|
8dcd6bc5ed | ||
|
|
87cb325369 | ||
|
|
e1743954d2 | ||
|
|
44b5b9fd5a | ||
|
|
68c5a3e2b7 | ||
|
|
684138c3fd | ||
|
|
0aa3241712 | ||
|
|
ad6358195e | ||
|
|
0e99669206 | ||
|
|
a91d98fb44 |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
19
.changeset/config.json
Normal file
19
.changeset/config.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "papra-hq/papra"}
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [
|
||||
["@papra/app-client", "@papra/app-server"]
|
||||
],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [],
|
||||
"privatePackages": {
|
||||
"tag": true,
|
||||
"version": true
|
||||
}
|
||||
}
|
||||
11
.github/workflows/ci-apps-papra-client.yaml
vendored
11
.github/workflows/ci-apps-papra-client.yaml
vendored
@@ -38,11 +38,12 @@ jobs:
|
||||
- name: Run unit test
|
||||
run: pnpm test
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
|
||||
# Ensure locales types are up to date
|
||||
# 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)
|
||||
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
|
||||
|
||||
|
||||
4
.github/workflows/ci-apps-papra-server.yaml
vendored
4
.github/workflows/ci-apps-papra-server.yaml
vendored
@@ -26,7 +26,9 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i --frozen-lockfile
|
||||
run: |
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm --filter "@papra/app-server^..." build
|
||||
|
||||
- name: Run linters
|
||||
run: pnpm lint
|
||||
|
||||
41
.github/workflows/ci-packages-api-sdk.yaml
vendored
Normal file
41
.github/workflows/ci-packages-api-sdk.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
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
|
||||
44
.github/workflows/ci-packages-cli.yaml
vendored
Normal file
44
.github/workflows/ci-packages-cli.yaml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
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
|
||||
41
.github/workflows/ci-packages-lecture.yaml
vendored
Normal file
41
.github/workflows/ci-packages-lecture.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: CI - Lecture
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci-packages-lecture:
|
||||
name: CI - Lecture
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/lecture
|
||||
|
||||
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
|
||||
41
.github/workflows/ci-packages-webhook.yaml
vendored
Normal file
41
.github/workflows/ci-packages-webhook.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
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
|
||||
25
.github/workflows/release-docker.yaml
vendored
25
.github/workflows/release-docker.yaml
vendored
@@ -1,9 +1,12 @@
|
||||
name: Release new versions
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -14,14 +17,6 @@ jobs:
|
||||
name: Release Docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get release version from tag
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Get release version from input
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
run: echo "RELEASE_VERSION=${{ github.event.inputs.release_version }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
@@ -53,9 +48,9 @@ jobs:
|
||||
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
|
||||
@@ -67,7 +62,7 @@ jobs:
|
||||
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
|
||||
53
.github/workflows/release.yml
vendored
Normal file
53
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
actions: write
|
||||
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: Create Release Pull Request
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
# Note: pnpm install after versioning is necessary to refresh lockfile
|
||||
version: pnpm run version
|
||||
publish: pnpm exec changeset publish
|
||||
commit: "chore(release): update versions"
|
||||
title: "chore(release): update versions"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Trigger Docker build
|
||||
if: steps.changesets.outputs.published == 'true' && contains(fromJson(steps.changesets.outputs.publishedPackages).*.name, '@papra/app-server')
|
||||
run: |
|
||||
VERSION=$(echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[] | select(.name=="@papra/app-server") | .version')
|
||||
echo "VERSION: $VERSION"
|
||||
gh workflow run release-docker.yaml -f version="$VERSION"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @papra-hq/papra-maintainers
|
||||
@@ -22,11 +22,11 @@ We follow a **GitHub Flow** model where all PRs should target the `main` branch,
|
||||
|
||||
- Each PR should be small and atomic. Please avoid solving multiple unrelated issues in a single PR.
|
||||
- Ensure that the **CI is green** before submitting. Some of the following checks are automatically run for each package: linting, type checking, testing, and building.
|
||||
- PRs without a corresponding issue are welcome.
|
||||
- If your PR fixes an issue, please reference the issue number in the PR description.
|
||||
- If your PR adds a new feature, please include tests and update the documentation if necessary.
|
||||
- Be prepared to address feedback and iterate on your PR.
|
||||
- Resolving merge conflicts is part of the PR author's responsibility.
|
||||
- Draft PRs are welcome to get feedback early on your work but only when requested, they'll not be reviewed.
|
||||
|
||||
### Branching
|
||||
|
||||
@@ -60,7 +60,10 @@ pnpm script:generate-i18n-types
|
||||
```
|
||||
|
||||
- This command will update the file [`locales.types.ts`](./apps/papra-client/src/modules/i18n/locale.types.ts) with the new/removed keys.
|
||||
- Will developing you can use `pnpm script:generate-i18n-types:watch` to automatically update the types when you touch the [`en.yml`](./apps/papra-client/src/locales/en.yml) file.
|
||||
- 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.
|
||||
|
||||
> [!TIP]
|
||||
> You can use the command `pnpm script:sync-i18n-key-order` to sync the order of the keys in the i18n files, it'll also add the missing keys as comments.
|
||||
|
||||
## Development Setup
|
||||
|
||||
@@ -81,7 +84,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 +102,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 +110,7 @@ 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`.
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
23
README.md
23
README.md
@@ -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
|
||||
|
||||
@@ -59,15 +56,21 @@ Papra is currently in **beta**. The core functionality is stable and usable, but
|
||||
- **Tags**: Organize your documents with tags.
|
||||
- **Email ingestion**: Send/forward emails to a generated address to automatically import documents.
|
||||
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
||||
- *In progress:* **i18n**: Support for multiple languages.
|
||||
- *Coming soon:* **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||
- *Coming soon:* **Folder ingestion**: Automatically import documents from a folder.
|
||||
- *Coming soon:* **SDK and API**: Build your own applications on top of Papra.
|
||||
- *Coming soon:* **CLI**: Manage your documents from the command line.
|
||||
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||
- **Folder ingestion**: Automatically import documents from a folder.
|
||||
- **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
|
||||
|
||||
|
||||
55
apps/docs/CHANGELOG.md
Normal file
55
apps/docs/CHANGELOG.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# @papra/docs
|
||||
|
||||
## 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
|
||||
|
||||
- [#280](https://github.com/papra-hq/papra/pull/280) [`85fa5c4`](https://github.com/papra-hq/papra/commit/85fa5c43424d139f5c2752a3ad644082e61d3d67) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix broken lint and added auto link check
|
||||
@@ -1,8 +1,11 @@
|
||||
import { env } from 'node:process';
|
||||
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;
|
||||
@@ -15,19 +18,20 @@ const posthogScript = posthogRawScript.replace('[POSTHOG-API-KEY]', posthogApiKe
|
||||
export default defineConfig({
|
||||
site: 'https://docs.papra.app',
|
||||
integrations: [
|
||||
UnoCSS(),
|
||||
starlight({
|
||||
plugins: [starlightThemeRapide()],
|
||||
plugins: [starlightThemeRapide(), starlightLinksValidator({ exclude: ['http://localhost:1221'] })],
|
||||
title: 'Papra Docs',
|
||||
logo: {
|
||||
dark: './src/assets/logo-dark.svg',
|
||||
light: './src/assets/logo-light.svg',
|
||||
alt: 'Papra Logo',
|
||||
},
|
||||
social: {
|
||||
github: 'https://github.com/papra-hq/papra',
|
||||
blueSky: 'https://bsky.app/profile/papra.app',
|
||||
discord: 'https://papra.app/discord',
|
||||
},
|
||||
social: [
|
||||
{ href: 'https://github.com/papra-hq/papra', icon: 'github', label: 'GitHub' },
|
||||
{ href: 'https://bsky.app/profile/papra.app', icon: 'blueSky', label: 'BlueSky' },
|
||||
{ href: 'https://papra.app/discord', icon: 'discord', label: 'Discord' },
|
||||
],
|
||||
expressiveCode: {
|
||||
themes: ['vitesse-black', 'vitesse-light'],
|
||||
},
|
||||
@@ -37,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
1382
apps/docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@papra/docs",
|
||||
"type": "module",
|
||||
"version": "0.3.0",
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra documentation website",
|
||||
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -17,10 +18,16 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.31.0",
|
||||
"astro": "^5.1.5",
|
||||
"@astrojs/solid-js": "^5.1.0",
|
||||
"@astrojs/starlight": "^0.34.3",
|
||||
"astro": "^5.8.0",
|
||||
"sharp": "^0.32.5",
|
||||
"starlight-theme-rapide": "^0.3.0",
|
||||
"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": {
|
||||
@@ -33,6 +40,7 @@
|
||||
"figue": "^2.2.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.6",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.7.3",
|
||||
"unocss": "0.65.0-beta.2"
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/docs/public/_headers
Normal file
3
apps/docs/public/_headers
Normal file
@@ -0,0 +1,3 @@
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
BIN
apps/docs/src/assets/api-key-creation-1.png
Normal file
BIN
apps/docs/src/assets/api-key-creation-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
apps/docs/src/assets/api-key-creation-2.png
Normal file
BIN
apps/docs/src/assets/api-key-creation-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -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;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ConfigDefinition, ConfigDefinitionElement } from 'figue';
|
||||
import { isArray, isEmpty, isNil } from 'lodash-es';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { configDefinition } from '../../papra-server/src/modules/config/config';
|
||||
|
||||
function walk(configDefinition: ConfigDefinition, path: string[] = []): (ConfigDefinitionElement & { path: string[] })[] {
|
||||
@@ -33,16 +35,19 @@ 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}
|
||||
### ${env}
|
||||
${documentation}
|
||||
|
||||
- Path: \`${path.join('.')}\`
|
||||
@@ -82,4 +87,18 @@ const fullDotEnv = rows.map(({ env, defaultValue, documentation }) => {
|
||||
].join('\n');
|
||||
}).join('\n\n');
|
||||
|
||||
export { fullDotEnv, mdSections };
|
||||
// Dirty hack to add the same anchors to the headings as the ones generated by Starlight
|
||||
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, '');
|
||||
};
|
||||
|
||||
const sectionsHtml = marked.parse(mdSections, { renderer });
|
||||
|
||||
export { fullDotEnv, mdSections, sectionsHtml };
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
@@ -12,7 +12,7 @@ This guide will show you how to setup a Cloudflare Email worker to receive email
|
||||
<Aside type="note">
|
||||
Setting up a Papra Intake Emails with a Cloudflare Email worker requires bit of knowledge about how to setup a CF Email worker and how to configure your Papra instance to receive emails.
|
||||
|
||||
For a simpler solution, you can use the official [OwlRelay integration](/docs/guides/intake-emails-with-papra-email-intake) guide.
|
||||
For a simpler solution, you can use the official [OwlRelay integration](/guides/intake-emails-with-owlrelay) guide.
|
||||
</Aside>
|
||||
|
||||
## Prerequisites
|
||||
@@ -23,7 +23,7 @@ In order to follow this guide, you need:
|
||||
- a publicly accessible Papra instance
|
||||
- basic development skills (git and node.js to setup the Email Worker)
|
||||
|
||||
If you prefer a simpler solution, you can use the official [OwlRelay integration](/docs/guides/intake-emails-with-papra-email-intake) guide.
|
||||
If you prefer a simpler solution, you can use the official [OwlRelay integration](/guides/intake-emails-with-owlrelay) guide.
|
||||
|
||||
## How it works
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ environment:
|
||||
|
||||
## Configuration
|
||||
|
||||
You can find the list of all configuration options in the [configuration reference](/docs/configuration-reference), the related variables are prefixed with `INGESTION_FOLDER_`.
|
||||
You can find the list of all configuration options in the [configuration reference](/self-hosting/configuration), the related variables are prefixed with `INGESTION_FOLDER_`.
|
||||
|
||||
|
||||
## Edge cases and behaviors
|
||||
|
||||
181
apps/docs/src/content/docs/03-guides/04-setup-custom-oauth2.mdx
Normal file
181
apps/docs/src/content/docs/03-guides/04-setup-custom-oauth2.mdx
Normal 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"]
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
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)
|
||||
|
||||
|
||||
|
||||
89
apps/docs/src/content/docs/04-resources/02-cli.mdx
Normal file
89
apps/docs/src/content/docs/04-resources/02-cli.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: CLI Documentation
|
||||
description: Learn how to use the Papra CLI to interact with your Papra instance from the command line.
|
||||
slug: resources/cli
|
||||
---
|
||||
|
||||
The Papra CLI is a command-line interface tool that helps you interact with the Papra platform from your terminal.
|
||||
|
||||
## Installation
|
||||
|
||||
For the moment, the CLI is only available as an NPM package.
|
||||
|
||||
```bash
|
||||
# using pnpm
|
||||
pnpm i -g @papra/cli
|
||||
|
||||
# or using npm
|
||||
npm i -g @papra/cli
|
||||
|
||||
# or using yarn
|
||||
yarn add -g @papra/cli
|
||||
```
|
||||
|
||||
The CLI will be installed globally, so you can use it from anywhere in your system with the `papra` command.
|
||||
|
||||
## Configuration
|
||||
|
||||
Before using the CLI, you need to configure it with your API credentials.
|
||||
|
||||
### Initial Setup
|
||||
|
||||
To initialize the configuration, run:
|
||||
|
||||
```bash
|
||||
papra config init
|
||||
```
|
||||
|
||||
This command will prompt you for:
|
||||
- **Instance URL**: Your Papra instance URL (e.g., `https://api.papra.app`)
|
||||
- **API Key**: Your personal API key (can be created in your User Settings)
|
||||
|
||||
### Managing Configuration
|
||||
|
||||
You can manage your configuration using the following commands:
|
||||
|
||||
- `papra config list`: View your current configuration
|
||||
- `papra config set api-key`: Set or update your API key
|
||||
- `papra config set api-url`: Set or update your instance URL
|
||||
- `papra config set default-org-id`: Set a default organization ID
|
||||
|
||||
### Organization IDs
|
||||
|
||||
Since Papra supports multiple organizations, you may need to specify the organization ID when importing documents for example. If want, you can set a default organization ID in your configuration.
|
||||
|
||||
```bash
|
||||
papra config set default-org-id <organization-id>
|
||||
papra documents import <file-path>
|
||||
|
||||
# or
|
||||
papra documents import -o <organization-id> <file-path>
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Importing documents
|
||||
|
||||
The `import` command allows you to import a document into your Papra organization.
|
||||
|
||||
```bash
|
||||
papra documents import -o <organization-id> <file-path>
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
For more information about any command, you can use the `--help` flag:
|
||||
|
||||
```bash
|
||||
papra --help
|
||||
papra config --help
|
||||
papra documents --help
|
||||
```
|
||||
|
||||
|
||||
## About the CLI
|
||||
|
||||
The CLI is built using the [citty](https://github.com/unjs/citty) framework and the [Papra TS SDK](https://github.com/papra-hq/papra/tree/main/packages/api-sdk).
|
||||
|
||||
|
||||
|
||||
210
apps/docs/src/content/docs/04-resources/03-api-endpoints.mdx
Normal file
210
apps/docs/src/content/docs/04-resources/03-api-endpoints.mdx
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
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>
|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 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)
|
||||
@@ -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.
|
||||
@@ -51,11 +51,11 @@ In today's digital world, managing countless important documents efficiently and
|
||||
- **Tags**: Organize your documents with tags.
|
||||
- **Email ingestion**: Send/forward emails to a generated address to automatically import documents.
|
||||
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
||||
- *In progress:* **i18n**: Support for multiple languages.
|
||||
- *Coming soon:* **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||
- *Coming soon:* **Folder ingestion**: Automatically import documents from a folder.
|
||||
- *Coming soon:* **SDK and API**: Build your own applications on top of Papra.
|
||||
- *Coming soon:* **CLI**: Manage your documents from the command line.
|
||||
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||
- **Folder ingestion**: Automatically import documents from a folder.
|
||||
- **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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { StarlightUserConfig } from '@astrojs/starlight/types';
|
||||
|
||||
export const sidebar: StarlightUserConfig['sidebar'] = [
|
||||
export const sidebar = [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
@@ -12,6 +12,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 +31,23 @@ 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: 'Resources',
|
||||
items: [
|
||||
{
|
||||
label: 'Troubleshooting',
|
||||
slug: 'resources/troubleshooting',
|
||||
},
|
||||
{
|
||||
label: 'CLI Documentation',
|
||||
slug: 'resources/cli',
|
||||
},
|
||||
{
|
||||
label: 'Security Policy',
|
||||
link: 'https://github.com/papra-hq/papra/blob/main/SECURITY.md',
|
||||
@@ -42,6 +55,10 @@ export const sidebar: StarlightUserConfig['sidebar'] = [
|
||||
target: '_blank',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'API Endpoints',
|
||||
slug: 'resources/api-endpoints',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
] satisfies StarlightUserConfig['sidebar'];
|
||||
|
||||
487
apps/docs/src/docker-compose-generator/dc-generator.astro
Normal file
487
apps/docs/src/docker-compose-generator/dc-generator.astro
Normal 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/pages/docker-compose-generator.astro
Normal file
16
apps/docs/src/pages/docker-compose-generator.astro
Normal 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>
|
||||
28
apps/docs/src/pages/docs-navigation.json.ts
Normal file
28
apps/docs/src/pages/docs-navigation.json.ts
Normal 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));
|
||||
};
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
||||
97
apps/docs/uno.config.ts
Normal file
97
apps/docs/uno.config.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
defineConfig,
|
||||
presetUno,
|
||||
transformerDirectives,
|
||||
transformerVariantGroup,
|
||||
} from 'unocss';
|
||||
import presetAnimations from 'unocss-preset-animations';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno({
|
||||
dark: {
|
||||
dark: '[data-kb-theme="dark"]',
|
||||
light: '[data-kb-theme="light"]',
|
||||
},
|
||||
prefix: '',
|
||||
}),
|
||||
presetAnimations(),
|
||||
],
|
||||
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',
|
||||
},
|
||||
});
|
||||
99
apps/papra-client/CHANGELOG.md
Normal file
99
apps/papra-client/CHANGELOG.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# @papra/app-client
|
||||
|
||||
## 0.6.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#377](https://github.com/papra-hq/papra/pull/377) [`205c6cf`](https://github.com/papra-hq/papra/commit/205c6cfd461fa0020a93753571f886726ddfdb57) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Improve file preview for text-like files (.env, yaml, extension-less text files,...)
|
||||
|
||||
- [#393](https://github.com/papra-hq/papra/pull/393) [`aad36f3`](https://github.com/papra-hq/papra/commit/aad36f325296548019148bc4e32782fe562fd95b) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix weird centering in document page for long filenames
|
||||
|
||||
- [#394](https://github.com/papra-hq/papra/pull/394) [`f28d824`](https://github.com/papra-hq/papra/commit/f28d8245bf385d7be3b3b8ee449c3fdc88fa375c) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added the possibility to disable login via email, to support sso-only auth
|
||||
|
||||
- [#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
|
||||
|
||||
- [#346](https://github.com/papra-hq/papra/pull/346) [`c54a71d`](https://github.com/papra-hq/papra/commit/c54a71d2c5998abde8ec78741b8c2e561203a045) Thanks [@blstmo](https://github.com/blstmo)! - Fixes 400 error when submitting tags with uppercase hex colour codes.
|
||||
|
||||
- [#408](https://github.com/papra-hq/papra/pull/408) [`09e3bc5`](https://github.com/papra-hq/papra/commit/09e3bc5e151594bdbcb1f9df1b869a78e583af3f) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added Romanian (ro) translation
|
||||
|
||||
- [#383](https://github.com/papra-hq/papra/pull/383) [`0b276ee`](https://github.com/papra-hq/papra/commit/0b276ee0d5e936fffc1f8284c654a8ada0efbafb) Thanks [@LMArantes](https://github.com/LMArantes)! - Added Brazilian Portuguese (pt-BR) language support
|
||||
|
||||
- [#399](https://github.com/papra-hq/papra/pull/399) [`47b69b1`](https://github.com/papra-hq/papra/commit/47b69b15f4f711e47421fc21a3ac447824d67642) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix back to organization link in organization settings
|
||||
|
||||
- [#403](https://github.com/papra-hq/papra/pull/403) [`1711ef8`](https://github.com/papra-hq/papra/commit/1711ef866d0071a804484b3e163a5e2ccbcec8fd) Thanks [@Icikowski](https://github.com/Icikowski)! - Added Polish (pl) language support
|
||||
|
||||
- [#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
|
||||
|
||||
- [#411](https://github.com/papra-hq/papra/pull/411) [`2601566`](https://github.com/papra-hq/papra/commit/26015666de197827a65a5bebf376921bbfcc3ab8) Thanks [@4DRIAN0RTIZ](https://github.com/4DRIAN0RTIZ)! - Added Spanish (es) translation
|
||||
|
||||
- [#391](https://github.com/papra-hq/papra/pull/391) [`40a1f91`](https://github.com/papra-hq/papra/commit/40a1f91b67d92e135d13dfcd41e5fd3532c30ca5) Thanks [@itsjuoum](https://github.com/itsjuoum)! - Added European Portuguese (pt) translation
|
||||
|
||||
- [#378](https://github.com/papra-hq/papra/pull/378) [`f1e1b40`](https://github.com/papra-hq/papra/commit/f1e1b4037b31ff5de1fd228b8390dd4d97a8bda8) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added tag color swatches and picker
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### 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
|
||||
|
||||
- [#359](https://github.com/papra-hq/papra/pull/359) [`0c2cf69`](https://github.com/papra-hq/papra/commit/0c2cf698d1a9e9a3cea023920b10cfcd5d83be14) Thanks [@Mavv3006](https://github.com/Mavv3006)! - Add German translation
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#333](https://github.com/papra-hq/papra/pull/333) [`ff830c2`](https://github.com/papra-hq/papra/commit/ff830c234a02ddb4cbc480cf77ef49b8de35fbae) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fixed version release link
|
||||
|
||||
## 0.6.1
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#317](https://github.com/papra-hq/papra/pull/317) [`79c1d32`](https://github.com/papra-hq/papra/commit/79c1d3206b140cf8b3d33ef8bda6098dcf4c9c9c) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added document activity log
|
||||
|
||||
- [#319](https://github.com/papra-hq/papra/pull/319) [`60059c8`](https://github.com/papra-hq/papra/commit/60059c895c4860cbfda69d3c989ad00542def65b) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added pending invitation management page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#309](https://github.com/papra-hq/papra/pull/309) [`d4f72e8`](https://github.com/papra-hq/papra/commit/d4f72e889a4d39214de998942bc0eb88cd5cee3d) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Disable "Manage subscription" from organization setting by default
|
||||
|
||||
- [#308](https://github.com/papra-hq/papra/pull/308) [`759a3ff`](https://github.com/papra-hq/papra/commit/759a3ff713db8337061418b9c9b122b957479343) Thanks [@CorentinTh](https://github.com/CorentinTh)! - I18n: full support for French language
|
||||
|
||||
- [#312](https://github.com/papra-hq/papra/pull/312) [`e5ef40f`](https://github.com/papra-hq/papra/commit/e5ef40f36c27ea25dc8a79ef2805d673761eec2a) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fixed an issue with the reset-password page navigation guard that prevented reset
|
||||
|
||||
## 0.5.1
|
||||
|
||||
## 0.5.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
|
||||
|
||||
- [#291](https://github.com/papra-hq/papra/pull/291) [`0627ec2`](https://github.com/papra-hq/papra/commit/0627ec25a422b7b820b08740cfc2905f9c55c00e) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added invitation system to add users to an organization
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#296](https://github.com/papra-hq/papra/pull/296) [`0ddc234`](https://github.com/papra-hq/papra/commit/0ddc2340f092cf6fe5bf2175b55fb46db7681c36) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix register page description
|
||||
|
||||
## 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
|
||||
@@ -5,6 +5,11 @@ export default antfu({
|
||||
semi: true,
|
||||
},
|
||||
|
||||
ignores: [
|
||||
// Generated file
|
||||
'src/modules/i18n/locales.types.ts',
|
||||
],
|
||||
|
||||
rules: {
|
||||
// To allow export on top of files
|
||||
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@papra/papra-app-client",
|
||||
"name": "@papra/app-client",
|
||||
"type": "module",
|
||||
"version": "0.3.0",
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"version": "0.6.4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra frontend client",
|
||||
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -26,50 +27,51 @@
|
||||
"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:generate-i18n-types:watch": "tsx watch --include src/locales/en.yml 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",
|
||||
"@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.81.2",
|
||||
"@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": "^1.255.1",
|
||||
"radix3": "^1.1.2",
|
||||
"solid-js": "^1.8.11",
|
||||
"solid-js": "^1.9.7",
|
||||
"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",
|
||||
"@iconify-json/tabler": "^1.2.19",
|
||||
"@playwright/test": "^1.53.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"jsdom": "^25.0.0",
|
||||
"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",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-solid": "^2.11.7",
|
||||
"vitest": "catalog:",
|
||||
"yaml": "^2.7.0"
|
||||
"yaml": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/papra-client/public/_headers
Normal file
3
apps/papra-client/public/_headers
Normal file
@@ -0,0 +1,3 @@
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
@@ -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';
|
||||
@@ -44,9 +45,11 @@ render(
|
||||
>
|
||||
<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>
|
||||
|
||||
|
||||
565
apps/papra-client/src/locales/de.yml
Normal file
565
apps/papra-client/src/locales/de.yml
Normal file
@@ -0,0 +1,565 @@
|
||||
# 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.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? Diese Aktion kann nicht rückgängig gemacht werden und alle mit dieser Organisation verbundenen Daten werden dauerhaft entfernt.
|
||||
organization.settings.delete.confirm.confirm-button: Organisation löschen
|
||||
organization.settings.delete.confirm.cancel-button: Abbrechen
|
||||
organization.settings.delete.success: Organisation gelöscht
|
||||
|
||||
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.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.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.document:created.description: Dokument erstellt
|
||||
webhooks.events.documents.document:deleted.description: Dokument gelöscht
|
||||
|
||||
# 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.intake-emails: E-Mail-Eingang
|
||||
layout.menu.webhooks: Webhooks
|
||||
layout.menu.members: Mitglieder
|
||||
layout.menu.invitations: Einladungen
|
||||
|
||||
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.file_too_big: Die Dokumentdatei ist zu groß
|
||||
api-errors.intake_email.limit_reached: Die maximale Anzahl an Eingangse-Mails 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
|
||||
|
||||
# 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
|
||||
@@ -1,219 +1,565 @@
|
||||
auth:
|
||||
request-password-reset:
|
||||
title: Reset your password
|
||||
description: Enter your email to reset your password.
|
||||
requested: If an account exists for this email, we've sent you an email to reset your password.
|
||||
back-to-login: Back to login
|
||||
form:
|
||||
email:
|
||||
label: Email
|
||||
placeholder: 'Example: ada@papra.app'
|
||||
required: Please enter your email address
|
||||
invalid: This email address is invalid
|
||||
submit: Request password reset
|
||||
# Authentication
|
||||
|
||||
reset-password:
|
||||
title: Reset your password
|
||||
description: Enter your new password to reset your password.
|
||||
reset: Your password has been reset.
|
||||
back-to-login: Back to login
|
||||
form:
|
||||
new-password:
|
||||
label: New password
|
||||
placeholder: 'Example: **********'
|
||||
required: Please enter your new password
|
||||
min-length: Password must be at least {{ minLength }} characters
|
||||
max-length: Password must be less than {{ maxLength }} characters
|
||||
submit: Reset password
|
||||
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
|
||||
|
||||
email-provider:
|
||||
open: Open {{ provider }}
|
||||
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
|
||||
|
||||
login:
|
||||
title: Login to Papra
|
||||
description: Enter your email or use social login to access your Papra account.
|
||||
login-with-provider: Login with {{ provider }}
|
||||
no-account: Don't have an account?
|
||||
register: Register
|
||||
form:
|
||||
email:
|
||||
label: Email
|
||||
placeholder: 'Example: ada@papra.app'
|
||||
required: Please enter your email address
|
||||
invalid: This email address is invalid
|
||||
password:
|
||||
label: Password
|
||||
placeholder: Set a password
|
||||
required: Please enter your password
|
||||
remember-me:
|
||||
label: Remember me
|
||||
forgot-password:
|
||||
label: Forgot password?
|
||||
submit: Login
|
||||
auth.email-provider.open: Open {{ provider }}
|
||||
|
||||
register:
|
||||
title: Register to Papra
|
||||
description: Enter your email or use social login to access your Papra account.
|
||||
register-with-email: Register with email
|
||||
register-with-provider: Register with {{ provider }}
|
||||
providers:
|
||||
google: Google
|
||||
github: GitHub
|
||||
have-account: Already have an account?
|
||||
login: Login
|
||||
registration-disabled:
|
||||
title: Registration is 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.
|
||||
form:
|
||||
email:
|
||||
label: Email
|
||||
placeholder: 'Example: ada@papra.app'
|
||||
required: Please enter your email address
|
||||
invalid: This email address is invalid
|
||||
password:
|
||||
label: Password
|
||||
placeholder: Set a password
|
||||
required: Please enter your password
|
||||
min-length: Password must be at least {{ minLength }} characters
|
||||
max-length: Password must be less than {{ maxLength }} characters
|
||||
name:
|
||||
label: Name
|
||||
placeholder: 'Example: Ada Lovelace'
|
||||
required: Please enter your name
|
||||
max-length: Name must be less than {{ maxLength }} characters
|
||||
submit: Register
|
||||
email-validation-required:
|
||||
title: Verify your email
|
||||
description: A verification email has been sent to your email address. Please verify your email address by clicking the link in the email.
|
||||
legal-links:
|
||||
description: By continuing, you acknowledge that you understand and agree to the {{ terms }} and {{ privacy }}.
|
||||
terms: Terms of Service
|
||||
privacy: Privacy Policy
|
||||
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
|
||||
|
||||
tags:
|
||||
no-tags:
|
||||
title: No tags yet
|
||||
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.
|
||||
create-tag: Create tag
|
||||
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
|
||||
|
||||
layout:
|
||||
menu:
|
||||
home: Home
|
||||
documents: Documents
|
||||
tags: Tags
|
||||
tagging-rules: Tagging rules
|
||||
integrations: Integrations
|
||||
deleted-documents: Deleted documents
|
||||
organization-settings: Organization settings
|
||||
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.
|
||||
|
||||
tagging-rules:
|
||||
field:
|
||||
name: document name
|
||||
content: document content
|
||||
operator:
|
||||
equals: equals
|
||||
not-equals: not equals
|
||||
contains: contains
|
||||
not-contains: not contains
|
||||
starts-with: starts with
|
||||
ends-with: ends with
|
||||
list:
|
||||
title: Tagging rules
|
||||
description: Manage your organization's tagging rules, to automatically tag documents based on conditions you define.
|
||||
demo-warning: 'Note: As this is a demo environment (with no server), tagging rules will not be applied to newly added documents.'
|
||||
no-tagging-rules:
|
||||
title: No tagging rules
|
||||
description: Create a tagging rule to automatically tag your added documents based on conditions you define.
|
||||
create-tagging-rule: Create tagging rule
|
||||
card:
|
||||
no-conditions: No conditions
|
||||
one-condition: 1 condition
|
||||
conditions: '{{ count }} conditions'
|
||||
delete: Delete rule
|
||||
edit: Edit rule
|
||||
create:
|
||||
title: Create tagging rule
|
||||
success: Tagging rule created successfully
|
||||
error: Failed to create tagging rule
|
||||
submit: Create rule
|
||||
form:
|
||||
name:
|
||||
label: Name
|
||||
placeholder: 'Example: Tag invoices'
|
||||
min-length: Please enter a name for the rule
|
||||
max-length: The name must be less than 64 characters
|
||||
description:
|
||||
label: Description
|
||||
placeholder: 'Example: Tag documents with "invoice" in the name'
|
||||
max-length: The description must be less than 256 characters
|
||||
conditions:
|
||||
label: Conditions
|
||||
description: Define the conditions that must be met for the rule to apply. All conditions must be met for the rule to apply.
|
||||
add-condition: Add condition
|
||||
no-conditions:
|
||||
title: No conditions
|
||||
description: You didn't add any conditions to this rule. This rule will apply its tags to all documents.
|
||||
confirm: Apply rule without conditions
|
||||
cancel: Cancel
|
||||
field:
|
||||
label: Field
|
||||
operator:
|
||||
label: Operator
|
||||
value:
|
||||
label: Value
|
||||
placeholder: 'Example: invoice'
|
||||
min-length: Please enter a value for the condition
|
||||
tags:
|
||||
label: Tags
|
||||
description: Select the tags to apply to the added documents that match the conditions
|
||||
min-length: At least one tag to apply is required
|
||||
add-tag: Create tag
|
||||
submit: Create rule
|
||||
update:
|
||||
title: Update tagging rule
|
||||
success: Tagging rule updated successfully
|
||||
error: Failed to update tagging rule
|
||||
submit: Update rule
|
||||
cancel: Cancel
|
||||
demo:
|
||||
popup:
|
||||
description: This is a demo environment, all data is save to your browser local storage.
|
||||
discord: Join the {{ discordLink }} to get support, propose features or just chat.
|
||||
discord-link-label: Discord server
|
||||
reset: Reset demo data
|
||||
hide: Hide
|
||||
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
|
||||
|
||||
trash:
|
||||
delete-all:
|
||||
button: Delete all
|
||||
confirm:
|
||||
title: Permanently delete all documents?
|
||||
description: Are you sure you want to permanently delete all documents from the trash? This action cannot be undone.
|
||||
label: Delete
|
||||
cancel: Cancel
|
||||
delete:
|
||||
button: Delete
|
||||
confirm:
|
||||
title: Permanently delete document?
|
||||
description: Are you sure you want to permanently delete this document from the trash? This action cannot be undone.
|
||||
label: Delete
|
||||
cancel: Cancel
|
||||
deleted:
|
||||
success:
|
||||
title: Document deleted
|
||||
description: The document has been permanently deleted.
|
||||
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.
|
||||
|
||||
import-documents:
|
||||
title:
|
||||
error: '{{ count }} documents failed'
|
||||
success: '{{ count }} documents imported'
|
||||
pending: '{{ count }} / {{ total }} documents imported'
|
||||
none: Import documents
|
||||
no-import-in-progress: No document import in progress
|
||||
# User settings
|
||||
|
||||
api-errors:
|
||||
document.already_exists: The document already exists
|
||||
document.file_too_big: The document file is too big
|
||||
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.
|
||||
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.
|
||||
default: An error occurred while processing your request.
|
||||
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.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? This action cannot be undone, and all data associated with this organization will be permanently removed.
|
||||
organization.settings.delete.confirm.confirm-button: Delete organization
|
||||
organization.settings.delete.confirm.cancel-button: Cancel
|
||||
organization.settings.delete.success: Organization deleted
|
||||
|
||||
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.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.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.document:created.description: Document created
|
||||
webhooks.events.documents.document:deleted.description: Document deleted
|
||||
|
||||
# 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.intake-emails: Intake emails
|
||||
layout.menu.webhooks: Webhooks
|
||||
layout.menu.members: Members
|
||||
layout.menu.invitations: Invitations
|
||||
|
||||
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.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-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
|
||||
|
||||
# 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
|
||||
|
||||
565
apps/papra-client/src/locales/es.yml
Normal file
565
apps/papra-client/src/locales/es.yml
Normal file
@@ -0,0 +1,565 @@
|
||||
# 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.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? Esta acción no se puede deshacer, y todos los datos asociados se eliminarán permanentemente.
|
||||
organization.settings.delete.confirm.confirm-button: Eliminar organización
|
||||
organization.settings.delete.confirm.cancel-button: Cancelar
|
||||
organization.settings.delete.success: Organización eliminada
|
||||
|
||||
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.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.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.document:created.description: Documento creado
|
||||
webhooks.events.documents.document:deleted.description: Documento eliminado
|
||||
|
||||
# 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.intake-emails: Correos de ingreso
|
||||
layout.menu.webhooks: Webhooks
|
||||
layout.menu.members: Miembros
|
||||
layout.menu.invitations: Invitaciones
|
||||
|
||||
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.file_too_big: El archivo del documento es demasiado grande
|
||||
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
|
||||
|
||||
# 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
|
||||
@@ -1,37 +1,565 @@
|
||||
auth:
|
||||
login:
|
||||
title: Connexion à Papra
|
||||
description: Entrez votre adresse e-mail ou utilisez un service tiers pour accéder à votre compte Papra.
|
||||
login-with-provider: Connexion via {{ provider }}
|
||||
no-account: Vous n'avez pas de compte ?
|
||||
register: S'inscrire
|
||||
email-validation-required:
|
||||
title: Vérifiez votre adresse e-mail
|
||||
description: Un e-mail de vérification a été envoyé à votre adresse. Veuillez vérifier votre adresse en cliquant sur le lien dans l'e-mail.
|
||||
legal-links:
|
||||
description: En continuant, vous reconnaissez que vous comprenez et acceptez les {{ terms }} et la {{ privacy }}.
|
||||
terms: Conditions d'utilisation
|
||||
privacy: Politique de confidentialité
|
||||
# Authentication
|
||||
|
||||
tags:
|
||||
no-tags:
|
||||
title: Aucun tag pour le moment
|
||||
description: Cette organisation n'a pas encore 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.
|
||||
create-tag: Créer un tag
|
||||
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
|
||||
|
||||
layout:
|
||||
menu:
|
||||
home: Accueil
|
||||
documents: Documents
|
||||
tags: Tags
|
||||
integrations: Intégrations
|
||||
deleted-documents: Documents supprimés
|
||||
organization-settings: Paramètres de l'organisation
|
||||
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
|
||||
|
||||
demo:
|
||||
popup:
|
||||
description: Ceci est un environnement de démo, toutes les données sont enregistrées dans le local storage de votre navigateur.
|
||||
discord: Rejoignez le {{ discordLink }} pour obtenir de l'aide, proposer des fonctionnalités ou discuter avec l'équipe.
|
||||
discord-link-label: Serveur Discord
|
||||
reset: Réinitialiser la démo
|
||||
hide: Masquer
|
||||
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.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 ? Cette action est irréversible, et toutes les données associées à cette organisation seront supprimées définitivement.
|
||||
organization.settings.delete.confirm.confirm-button: Supprimer l'organisation
|
||||
organization.settings.delete.confirm.cancel-button: Annuler
|
||||
organization.settings.delete.success: Organisation supprimée
|
||||
|
||||
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: Created
|
||||
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: Status
|
||||
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.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.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.document:created.description: Document créé
|
||||
webhooks.events.documents.document:deleted.description: Document supprimé
|
||||
|
||||
# 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.intake-emails: Adresses de réception
|
||||
layout.menu.webhooks: Webhooks
|
||||
layout.menu.members: Membres
|
||||
layout.menu.invitations: Invitations
|
||||
|
||||
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.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-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
|
||||
|
||||
# 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
|
||||
|
||||
565
apps/papra-client/src/locales/pl.yml
Normal file
565
apps/papra-client/src/locales/pl.yml
Normal file
@@ -0,0 +1,565 @@
|
||||
# Authentication
|
||||
|
||||
auth.request-password-reset.title: Zresetuj swoje hasło
|
||||
auth.request-password-reset.description: Wprowadź swój adres e-mail, aby zresetować hasło.
|
||||
auth.request-password-reset.requested: Jeśli istnieje konto powiązane z tym adresem e-mail, otrzymasz wiadomość umożliwiającą zresetowanie hasła.
|
||||
auth.request-password-reset.back-to-login: Wróć do logowania
|
||||
auth.request-password-reset.form.email.label: E-mail
|
||||
auth.request-password-reset.form.email.placeholder: 'Przykład: ada@papra.app'
|
||||
auth.request-password-reset.form.email.required: Wprowadź swój adres e-mail
|
||||
auth.request-password-reset.form.email.invalid: Ten adres e-mail jest nieprawidłowy
|
||||
auth.request-password-reset.form.submit: Poproś o zresetowanie hasła
|
||||
|
||||
auth.reset-password.title: Zresetuj swoje hasło
|
||||
auth.reset-password.description: Wprowadź nowe hasło, aby zresetować dotychczasowe.
|
||||
auth.reset-password.reset: Twoje hasło zostało zresetowane.
|
||||
auth.reset-password.back-to-login: Wróć do logowania
|
||||
auth.reset-password.form.new-password.label: Nowe hasło
|
||||
auth.reset-password.form.new-password.placeholder: 'Przykład: **********'
|
||||
auth.reset-password.form.new-password.required: Wprowadź nowe hasło
|
||||
auth.reset-password.form.new-password.min-length: Hasło musi mieć co najmniej {{ minLength }} znaków
|
||||
auth.reset-password.form.new-password.max-length: Hasło musi mieć mniej niż {{ maxLength }} znaków
|
||||
auth.reset-password.form.submit: Zresetuj hasło
|
||||
|
||||
auth.email-provider.open: Otwórz {{ provider }}
|
||||
|
||||
auth.login.title: Zaloguj się do Papra
|
||||
auth.login.description: Wprowadź swój adres e-mail lub skorzystaj z logowania federacyjnego, aby uzyskać dostęp do swojego konta Papra.
|
||||
auth.login.login-with-provider: Zaloguj się za pomocą {{ provider }}
|
||||
auth.login.no-account: Nie masz konta?
|
||||
auth.login.register: Zarejestruj się
|
||||
auth.login.form.email.label: E-mail
|
||||
auth.login.form.email.placeholder: 'Przykład: ada@papra.app'
|
||||
auth.login.form.email.required: Wprowadź swój adres e-mail
|
||||
auth.login.form.email.invalid: Ten adres e-mail jest nieprawidłowy
|
||||
auth.login.form.password.label: Hasło
|
||||
auth.login.form.password.placeholder: Ustaw hasło
|
||||
auth.login.form.password.required: Wprowadź swoje hasło
|
||||
auth.login.form.remember-me.label: Zapamiętaj mnie
|
||||
auth.login.form.forgot-password.label: Zapomniałeś hasła?
|
||||
auth.login.form.submit: Zaloguj się
|
||||
|
||||
auth.register.title: Zarejestruj się w Papra
|
||||
auth.register.description: Utwórz konto, aby zacząć korzystać z Papra.
|
||||
auth.register.register-with-email: Zarejestruj się przez e-mail
|
||||
auth.register.register-with-provider: Zarejestruj się przez {{ provider }}
|
||||
auth.register.providers.google: Google
|
||||
auth.register.providers.github: GitHub
|
||||
auth.register.have-account: Masz już konto?
|
||||
auth.register.login: Zaloguj się
|
||||
auth.register.registration-disabled.title: Rejestracja jest wyłączona
|
||||
auth.register.registration-disabled.description: Tworzenie nowych kont na tej instancji Papra jest obecnie wyłączone. Tylko użytkownicy z istniejącymi kontami mogą się zalogować. Jeśli uważasz, że to błąd, skontaktuj się z administratorem tej instancji.
|
||||
auth.register.form.email.label: E-mail
|
||||
auth.register.form.email.placeholder: 'Przykład: ada@papra.app'
|
||||
auth.register.form.email.required: Wprowadź swój adres e-mail
|
||||
auth.register.form.email.invalid: Ten adres e-mail jest nieprawidłowy
|
||||
auth.register.form.password.label: Hasło
|
||||
auth.register.form.password.placeholder: Ustaw hasło
|
||||
auth.register.form.password.required: Wprowadź swoje hasło
|
||||
auth.register.form.password.min-length: Hasło musi mieć co najmniej {{ minLength }} znaków
|
||||
auth.register.form.password.max-length: Hasło musi mieć mniej niż {{ maxLength }} znaków
|
||||
auth.register.form.name.label: Imię i nazwisko
|
||||
auth.register.form.name.placeholder: 'Przykład: Ada Lovelace'
|
||||
auth.register.form.name.required: Wprowadź swoje imię i nazwisko
|
||||
auth.register.form.name.max-length: Imię i nazwisko musi mieć mniej niż {{ maxLength }} znaków
|
||||
auth.register.form.submit: Zarejestruj się
|
||||
|
||||
auth.email-validation-required.title: Zweryfikuj swój adres e-mail
|
||||
auth.email-validation-required.description: Wiadomość weryfikacyjna została wysłana na Twój adres e-mail. Zweryfikuj swój adres e-mail, klikając link w wiadomości.
|
||||
|
||||
auth.legal-links.description: Kontynuując, potwierdzasz, że rozumiesz i zgadzasz się na {{ terms }} oraz {{ privacy }}.
|
||||
auth.legal-links.terms: Warunki korzystania z usługi
|
||||
auth.legal-links.privacy: Polityka prywatności
|
||||
|
||||
auth.no-auth-provider.title: Brak dostawcy uwierzytelniania
|
||||
auth.no-auth-provider.description: Na tej instancji Papra nie ma włączonych dostawców uwierzytelniania. Skontaktuj się z administratorem tej instancji, aby je włączyć.
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: Ustawienia użytkownika
|
||||
user.settings.description: Zarządzaj ustawieniami swojego konta.
|
||||
|
||||
user.settings.email.title: Adres e-mail
|
||||
user.settings.email.description: Twój adres e-mail nie może być zmieniony.
|
||||
user.settings.email.label: Adres e-mail
|
||||
|
||||
user.settings.name.title: Imię i nazwisko
|
||||
user.settings.name.description: Twoje imię i nazwisko jest wyświetlane innym członkom organizacji.
|
||||
user.settings.name.label: Imię i nazwisko
|
||||
user.settings.name.placeholder: 'Przykład: Jan Kowalski'
|
||||
user.settings.name.update: Zaktualizuj imię i nazwisko
|
||||
user.settings.name.updated: Twoje imię i nazwisko zostało zaktualizowane
|
||||
|
||||
user.settings.logout.title: Wyloguj się
|
||||
user.settings.logout.description: Wyloguj się ze swojego konta. Możesz zalogować się ponownie później.
|
||||
user.settings.logout.button: Wyloguj się
|
||||
|
||||
# Organizations
|
||||
|
||||
organizations.list.title: Twoje organizacje
|
||||
organizations.list.description: Organizacje to sposób grupowania dokumentów i zarządzania dostępem do nich. Możesz tworzyć wiele organizacji i zapraszać członków zespołu do współpracy.
|
||||
organizations.list.create-new: Utwórz nową organizację
|
||||
|
||||
organizations.details.no-documents.title: Brak dokumentów
|
||||
organizations.details.no-documents.description: W tej organizacji nie ma jeszcze żadnych dokumentów. Zacznij od przesłania kilku dokumentów.
|
||||
organizations.details.upload-documents: Prześlij dokumenty
|
||||
organizations.details.documents-count: dokumentów w sumie
|
||||
organizations.details.total-size: całkowity rozmiar
|
||||
organizations.details.latest-documents: Najnowsze zaimportowane dokumenty
|
||||
|
||||
organizations.create.title: Utwórz nową organizację
|
||||
organizations.create.description: Twoje dokumenty będą grupowane według organizacji. Możesz tworzyć wiele organizacji, aby oddzielić swoje dokumenty, na przykład dokumenty osobiste i służbowe.
|
||||
organizations.create.back: Wstecz
|
||||
organizations.create.error.max-count-reached: Osiągnąłeś maksymalną liczbę organizacji, które możesz utworzyć. Jeśli potrzebujesz utworzyć więcej, skontaktuj się z pomocą techniczną.
|
||||
organizations.create.form.name.label: Nazwa organizacji
|
||||
organizations.create.form.name.placeholder: 'Przykład: Acme Inc.'
|
||||
organizations.create.form.name.required: Wprowadź nazwę organizacji
|
||||
organizations.create.form.submit: Utwórz organizację
|
||||
organizations.create.success: Organizacja została pomyślnie utworzona
|
||||
|
||||
organizations.create-first.title: Utwórz swoją organizację
|
||||
organizations.create-first.description: Twoje dokumenty będą grupowane według organizacji. Możesz tworzyć wiele organizacji, aby oddzielić swoje dokumenty, na przykład dokumenty osobiste i służbowe.
|
||||
organizations.create-first.default-name: Moja organizacja
|
||||
organizations.create-first.user-name: 'Organizacja użytkownika {{ name }}'
|
||||
|
||||
organization.settings.title: Ustawienia organizacji
|
||||
organization.settings.page.title: Ustawienia organizacji
|
||||
organization.settings.page.description: Zarządzaj ustawieniami swojej organizacji.
|
||||
organization.settings.name.title: Nazwa organizacji
|
||||
organization.settings.name.update: Zaktualizuj nazwę
|
||||
organization.settings.name.placeholder: 'Przykład: Acme Inc.'
|
||||
organization.settings.name.updated: Nazwa organizacji została zaktualizowana
|
||||
organization.settings.subscription.title: Subskrypcja
|
||||
organization.settings.subscription.description: Zarządzaj swoim rozliczeniem, fakturami i metodami płatności.
|
||||
organization.settings.subscription.manage: Zarządzaj subskrypcją
|
||||
organization.settings.subscription.error: Nie udało się uzyskać adresu URL portalu klienta
|
||||
organization.settings.delete.title: Usuń organizację
|
||||
organization.settings.delete.description: Usunięcie tej organizacji spowoduje trwałe usunięcie wszystkich danych z nią związanych.
|
||||
organization.settings.delete.confirm.title: Usuń organizację
|
||||
organization.settings.delete.confirm.message: Czy na pewno chcesz usunąć tę organizację? Ta operacja jest nieodwracalna, a wszystkie dane związane z tą organizacją zostaną trwale usunięte.
|
||||
organization.settings.delete.confirm.confirm-button: Usuń organizację
|
||||
organization.settings.delete.confirm.cancel-button: Anuluj
|
||||
organization.settings.delete.success: Organizacja została usunięta
|
||||
|
||||
organizations.members.title: Członkowie
|
||||
organizations.members.description: Zarządzaj członkami swojej organizacji
|
||||
organizations.members.invite-member: Zaproś członka
|
||||
organizations.members.invite-member-disabled-tooltip: Tylko administratorzy lub właściciele mogą zapraszać członków do organizacji
|
||||
organizations.members.remove-from-organization: Usuń z organizacji
|
||||
organizations.members.role: Rola
|
||||
organizations.members.roles.owner: Właściciel
|
||||
organizations.members.roles.admin: Administrator
|
||||
organizations.members.roles.member: Członek
|
||||
organizations.members.delete.confirm.title: Usuń członka
|
||||
organizations.members.delete.confirm.message: Czy na pewno chcesz usunąć tego członka z organizacji?
|
||||
organizations.members.delete.confirm.confirm-button: Usuń
|
||||
organizations.members.delete.confirm.cancel-button: Anuluj
|
||||
organizations.members.delete.success: Członek został usunięty z organizacji
|
||||
organizations.members.update-role.success: Rola członka została zaktualizowana
|
||||
organizations.members.table.headers.name: Imię i nazwisko
|
||||
organizations.members.table.headers.email: E-mail
|
||||
organizations.members.table.headers.role: Rola
|
||||
organizations.members.table.headers.created: Utworzono
|
||||
organizations.members.table.headers.actions: Akcje
|
||||
|
||||
organizations.invite-member.title: Zaproś członka
|
||||
organizations.invite-member.description: Zaproś członka do swojej organizacji
|
||||
organizations.invite-member.form.email.label: E-mail
|
||||
organizations.invite-member.form.email.placeholder: 'Przykład: ada@papra.app'
|
||||
organizations.invite-member.form.email.required: Wprowadź poprawny adres e-mail
|
||||
organizations.invite-member.form.role.label: Rola
|
||||
organizations.invite-member.form.submit: Zaproś do organizacji
|
||||
organizations.invite-member.success.message: Członek zaproszony
|
||||
organizations.invite-member.success.description: E-mail został zaproszony do organizacji.
|
||||
organizations.invite-member.error.message: Nie udało się zaprosić członka
|
||||
|
||||
organizations.invitations.title: Zaproszenia
|
||||
organizations.invitations.description: Zarządzaj zaproszeniami do swojej organizacji
|
||||
organizations.invitations.list.cta: Zaproś członka
|
||||
organizations.invitations.list.empty.title: Brak oczekujących zaproszeń
|
||||
organizations.invitations.list.empty.description: Nie zostałeś zaproszony do żadnej organizacji.
|
||||
organizations.invitations.status.pending: Oczekujące
|
||||
organizations.invitations.status.accepted: Zaakceptowane
|
||||
organizations.invitations.status.rejected: Odrzucone
|
||||
organizations.invitations.status.expired: Wygasłe
|
||||
organizations.invitations.status.cancelled: Anulowane
|
||||
organizations.invitations.resend: Wyślij zaproszenie ponownie
|
||||
organizations.invitations.cancel.title: Anuluj zaproszenie
|
||||
organizations.invitations.cancel.description: Czy na pewno chcesz anulować to zaproszenie?
|
||||
organizations.invitations.cancel.confirm: Anuluj zaproszenie
|
||||
organizations.invitations.cancel.cancel: Anuluj
|
||||
organizations.invitations.resend.title: Wyślij zaproszenie ponownie
|
||||
organizations.invitations.resend.description: Czy na pewno chcesz wysłać ponownie to zaproszenie? To spowoduje wysłanie nowego e-maila do odbiorcy.
|
||||
organizations.invitations.resend.confirm: Wyślij zaproszenie ponownie
|
||||
organizations.invitations.resend.cancel: Anuluj
|
||||
|
||||
invitations.list.title: Zaproszenia
|
||||
invitations.list.description: Zarządzaj zaproszeniami do swojej organizacji
|
||||
invitations.list.empty.title: Brak oczekujących zaproszeń
|
||||
invitations.list.empty.description: Nie zostałeś zaproszony do żadnej organizacji.
|
||||
invitations.list.headers.organization: Organizacja
|
||||
invitations.list.headers.status: Status
|
||||
invitations.list.headers.created: Utworzono
|
||||
invitations.list.headers.actions: Akcje
|
||||
invitations.list.actions.accept: Zaakceptuj
|
||||
invitations.list.actions.reject: Odrzuć
|
||||
invitations.list.actions.accept.success.message: Zaproszenie zaakceptowane
|
||||
invitations.list.actions.accept.success.description: Zaproszenie zostało zaakceptowane.
|
||||
invitations.list.actions.reject.success.message: Zaproszenie odrzucone
|
||||
invitations.list.actions.reject.success.description: Zaproszenie zostało odrzucone.
|
||||
|
||||
# Documents
|
||||
|
||||
documents.list.title: Dokumenty
|
||||
documents.list.no-documents.title: Brak dokumentów
|
||||
documents.list.no-documents.description: W tej organizacji nie ma jeszcze żadnych dokumentów. Zacznij od przesłania kilku dokumentów.
|
||||
documents.list.no-results: Nie znaleziono dokumentów
|
||||
|
||||
documents.tabs.info: Informacje
|
||||
documents.tabs.content: Treść
|
||||
documents.tabs.activity: Aktywność
|
||||
documents.deleted.message: Ten dokument został usunięty i zostanie trwale usunięty za {{ days }} dni.
|
||||
documents.actions.download: Pobierz
|
||||
documents.actions.open-in-new-tab: Otwórz w nowej karcie
|
||||
documents.actions.restore: Przywróć
|
||||
documents.actions.delete: Usuń
|
||||
documents.actions.edit: Edytuj
|
||||
documents.actions.cancel: Anuluj
|
||||
documents.actions.save: Zapisz
|
||||
documents.actions.saving: Zapisywanie...
|
||||
documents.content.alert: Zawartość dokumentu jest automatycznie wyodrębniana z dokumentu podczas przesyłania. Jest używana tylko do wyszukiwania i indeksowania.
|
||||
documents.info.id: ID
|
||||
documents.info.name: Nazwa
|
||||
documents.info.type: Typ
|
||||
documents.info.size: Rozmiar
|
||||
documents.info.created-at: Utworzono
|
||||
documents.info.updated-at: Zaktualizowano
|
||||
documents.info.never: Nigdy
|
||||
|
||||
documents.rename.title: Zmień nazwę dokumentu
|
||||
documents.rename.form.name.label: Nazwa
|
||||
documents.rename.form.name.placeholder: 'Przykład: Faktura 2024'
|
||||
documents.rename.form.name.required: Proszę wprowadzić nazwę dokumentu
|
||||
documents.rename.form.name.max-length: Nazwa musi mieć mniej niż 255 znaków
|
||||
documents.rename.form.submit: Zmień nazwę dokumentu
|
||||
documents.rename.success: Nazwa dokumentu została pomyślnie zmieniona
|
||||
documents.rename.cancel: Anuluj
|
||||
|
||||
import-documents.title.error: '{{ count }} dokumentów nie powiodły się'
|
||||
import-documents.title.success: '{{ count }} dokumentów zaimportowane'
|
||||
import-documents.title.pending: '{{ count }} / {{ total }} dokumentów zaimportowanych'
|
||||
import-documents.title.none: Importuj dokumenty
|
||||
import-documents.no-import-in-progress: Brak importu dokumentów w toku
|
||||
|
||||
documents.deleted.title: Usunięte dokumenty
|
||||
documents.deleted.empty.title: Brak usuniętych dokumentów
|
||||
documents.deleted.empty.description: Nie masz żadnych usuniętych dokumentów. Dokumenty, które są usuwane, zostaną przeniesione do kosza na {{ days }} dni.
|
||||
documents.deleted.retention-notice: Wszystkie usunięte dokumenty są przechowywane w koszu przez {{ days }} dni. Po upływie tego terminu dokumenty zostaną trwale usunięte, a Ty nie będziesz mógł ich przywrócić.
|
||||
documents.deleted.deleted-at: Usunięto
|
||||
documents.deleted.restoring: Przywracanie...
|
||||
documents.deleted.deleting: Usuwanie...
|
||||
|
||||
documents.preview.unknown-file-type: Brak podglądu dla tego typu pliku
|
||||
documents.preview.binary-file: To wydaje się być plikiem binarnym i nie może być wyświetlane jako tekst
|
||||
|
||||
trash.delete-all.button: Usuń wszystkie
|
||||
trash.delete-all.confirm.title: Trwale usunąć wszystkie dokumenty?
|
||||
trash.delete-all.confirm.description: Czy na pewno chcesz trwale usunąć wszystkie dokumenty z kosza? Ta akcja nie może być cofnięta.
|
||||
trash.delete-all.confirm.label: Usuń
|
||||
trash.delete-all.confirm.cancel: Anuluj
|
||||
trash.delete.button: Usuń
|
||||
trash.delete.confirm.title: Trwale usunąć dokument?
|
||||
trash.delete.confirm.description: Czy na pewno chcesz trwale usunąć ten dokument z kosza? Ta akcja nie może być cofnięta.
|
||||
trash.delete.confirm.label: Usuń
|
||||
trash.delete.confirm.cancel: Anuluj
|
||||
trash.deleted.success.title: Dokument usunięty
|
||||
trash.deleted.success.description: Dokument został trwale usunięty.
|
||||
|
||||
activity.document.created: Dokument został utworzony
|
||||
activity.document.updated.single: Pole {{ field }} zostało zaktualizowane
|
||||
activity.document.updated.multiple: Pola {{ fields }} zostały zaktualizowane
|
||||
activity.document.updated: Dokument został zaktualizowany
|
||||
activity.document.deleted: Dokument został usunięty
|
||||
activity.document.restored: Dokument został przywrócony
|
||||
activity.document.tagged: Tag {{ tag }} został dodany
|
||||
activity.document.untagged: Tag {{ tag }} został usunięty
|
||||
|
||||
activity.document.user.name: od {{ name }}
|
||||
|
||||
activity.load-more: Załaduj więcej
|
||||
activity.no-more-activities: Brak dalszych działań dla tego dokumentu
|
||||
|
||||
# Tags
|
||||
|
||||
tags.no-tags.title: Brak tagów
|
||||
tags.no-tags.description: Ta organizacja nie ma jeszcze tagów. Tagi służą do kategoryzowania dokumentów. Możesz dodać tagi do swoich dokumentów, aby ułatwić ich wyszukiwanie i organizację.
|
||||
tags.no-tags.create-tag: Utwórz tag
|
||||
|
||||
tags.title: Tagi dokumentów
|
||||
tags.description: Tagi służą do kategoryzowania dokumentów. Możesz dodać tagi do swoich dokumentów, aby ułatwić ich wyszukiwanie i organizację.
|
||||
tags.create: Utwórz tag
|
||||
tags.update: Zaktualizuj tag
|
||||
tags.delete: Usuń tag
|
||||
tags.delete.confirm.title: Usuń tag
|
||||
tags.delete.confirm.message: Czy na pewno chcesz usunąć ten tag? Usunięcie tagu spowoduje jego usunięcie ze wszystkich dokumentów.
|
||||
tags.delete.confirm.confirm-button: Usuń
|
||||
tags.delete.confirm.cancel-button: Anuluj
|
||||
tags.delete.success: Tag został pomyślnie usunięty
|
||||
tags.create.success: Tag "{{ name }}" został pomyślnie utworzony.
|
||||
tags.update.success: Tag "{{ name }}" został pomyślnie zaktualizowany.
|
||||
tags.form.name.label: Nazwa
|
||||
tags.form.name.placeholder: 'Przykład: Umowy'
|
||||
tags.form.name.required: Proszę wprowadzić nazwę tagu
|
||||
tags.form.name.max-length: Nazwa tagu musi mieć mniej niż 64 znaki
|
||||
tags.form.color.label: Kolor
|
||||
tags.form.color.required: Proszę wprowadzić kolor
|
||||
tags.form.color.invalid: Kolor hex jest źle sformatowany.
|
||||
tags.form.description.label: Opis
|
||||
tags.form.description.optional: (opcjonalnie)
|
||||
tags.form.description.placeholder: 'Przykład: Wszystkie umowy podpisane przez firmę'
|
||||
tags.form.description.max-length: Opis musi mieć mniej niż 256 znaków
|
||||
tags.form.no-description: Brak opisu
|
||||
tags.table.headers.tag: Tag
|
||||
tags.table.headers.description: Opis
|
||||
tags.table.headers.documents: Dokumenty
|
||||
tags.table.headers.created: Utworzono
|
||||
tags.table.headers.actions: Akcje
|
||||
|
||||
# Tagging rules
|
||||
|
||||
tagging-rules.field.name: nazwa dokumentu
|
||||
tagging-rules.field.content: treść dokumentu
|
||||
tagging-rules.operator.equals: równa się
|
||||
tagging-rules.operator.not-equals: nie równa się
|
||||
tagging-rules.operator.contains: zawiera
|
||||
tagging-rules.operator.not-contains: nie zawiera
|
||||
tagging-rules.operator.starts-with: zaczyna się od
|
||||
tagging-rules.operator.ends-with: kończy się na
|
||||
tagging-rules.list.title: Reguły tagowania
|
||||
tagging-rules.list.description: Zarządzaj regułami tagowania w swojej organizacji, aby automatycznie tagować dokumenty na podstawie zdefiniowanych przez siebie warunków.
|
||||
tagging-rules.list.demo-warning: 'Uwaga: Ponieważ jest to środowisko demonstracyjne (bez serwera), reguły tagowania nie będą stosowane do nowo dodanych dokumentów.'
|
||||
tagging-rules.list.no-tagging-rules.title: Brak reguł tagowania
|
||||
tagging-rules.list.no-tagging-rules.description: Utwórz regułę tagowania, aby automatycznie tagować dodane dokumenty na podstawie zdefiniowanych przez siebie warunków.
|
||||
tagging-rules.list.no-tagging-rules.create-tagging-rule: Utwórz regułę tagowania
|
||||
tagging-rules.list.card.no-conditions: Brak warunków
|
||||
tagging-rules.list.card.one-condition: 1 warunek
|
||||
tagging-rules.list.card.conditions: '{{ count }} warunków'
|
||||
tagging-rules.list.card.delete: Usuń regułę
|
||||
tagging-rules.list.card.edit: Edytuj regułę
|
||||
tagging-rules.create.title: Utwórz regułę tagowania
|
||||
tagging-rules.create.success: Reguła tagowania została pomyślnie utworzona
|
||||
tagging-rules.create.error: Nie udało się utworzyć reguły tagowania
|
||||
tagging-rules.create.submit: Utwórz regułę
|
||||
tagging-rules.form.name.label: Nazwa
|
||||
tagging-rules.form.name.placeholder: 'Przykład: Taguj faktury'
|
||||
tagging-rules.form.name.min-length: Proszę wprowadzić nazwę reguły
|
||||
tagging-rules.form.name.max-length: Nazwa musi mieć mniej niż 64 znaki
|
||||
tagging-rules.form.description.label: Opis
|
||||
tagging-rules.form.description.placeholder: "Przykład: Oznacz dokumenty ze słowem 'faktura' w nazwie"
|
||||
tagging-rules.form.description.max-length: Opis musi mieć mniej niż 256 znaków
|
||||
tagging-rules.form.conditions.label: Warunki
|
||||
tagging-rules.form.conditions.description: Zdefiniuj warunki, które muszą być spełnione, aby reguła mogła zostać zastosowana. Wszystkie warunki muszą być spełnione, aby reguła mogła zostać zastosowana.
|
||||
tagging-rules.form.conditions.add-condition: Dodaj warunek
|
||||
tagging-rules.form.conditions.no-conditions.title: Brak warunków
|
||||
tagging-rules.form.conditions.no-conditions.description: Nie dodałeś żadnych warunków do tej reguły. Ta reguła zastosuje swoje tagi do wszystkich dokumentów.
|
||||
tagging-rules.form.conditions.no-conditions.confirm: Zastosuj regułę bez warunków
|
||||
tagging-rules.form.conditions.no-conditions.cancel: Anuluj
|
||||
tagging-rules.form.conditions.value.placeholder: 'Przykład: faktura'
|
||||
tagging-rules.form.conditions.value.min-length: Proszę wprowadzić wartość dla warunku
|
||||
tagging-rules.form.tags.label: Tagi
|
||||
tagging-rules.form.tags.description: Wybierz tagi do zastosowania do dodanych dokumentów, które spełniają warunki
|
||||
tagging-rules.form.tags.min-length: Co najmniej jeden tag do zastosowania jest wymagany
|
||||
tagging-rules.form.tags.add-tag: Utwórz tag
|
||||
tagging-rules.form.submit: Utwórz regułę
|
||||
tagging-rules.update.title: Zaktualizuj regułę tagowania
|
||||
tagging-rules.update.error: Nie udało się zaktualizować reguły tagowania
|
||||
tagging-rules.update.submit: Zaktualizuj regułę
|
||||
tagging-rules.update.cancel: Anuluj
|
||||
|
||||
# Intake emails
|
||||
|
||||
intake-emails.title: Adresy przyjęć
|
||||
intake-emails.description: Adresy przyjęć służą do automatycznego przyjmowania wiadomości e-mail do Papra. Wystarczy przekazać wiadomości e-mail na adres e-mail do przyjmowania, a ich załączniki zostaną dodane do dokumentów Twojej organizacji.
|
||||
intake-emails.disabled.title: Adresy przyjęć są wyłączone
|
||||
intake-emails.disabled.description: Adresy przyjęć są wyłączone na tej instancji. Skontaktuj się z administratorem, aby je włączyć. Zobacz {{ documentation }} w celu uzyskania dodatkowych informacji.
|
||||
intake-emails.disabled.documentation: dokumentację
|
||||
intake-emails.info: Tylko włączone adresy przyjęć z dozwolonych źródeł będą przetwarzane. Możesz w dowolnym momencie włączyć lub wyłączyć adres e-mail do przyjęć.
|
||||
intake-emails.empty.title: Brak adresów przyjęć
|
||||
intake-emails.empty.description: Wygeneruj adres przyjęć, aby łatwo przyjmować załączniki e-mail.
|
||||
intake-emails.empty.generate: Wygeneruj adres e-mail do przyjęć
|
||||
intake-emails.count: '{{ count }} adres/ów e-mail do przyjęć dla tej organizacji'
|
||||
intake-emails.new: Nowy adres e-mail do przyjęć
|
||||
intake-emails.disabled-label: (Wyłączone)
|
||||
intake-emails.no-origins: Brak dozwolonych źródeł e-mail
|
||||
intake-emails.allowed-origins: Dozwolone z {{ count }} adresu/ów
|
||||
intake-emails.actions.enable: Włącz
|
||||
intake-emails.actions.disable: Wyłącz
|
||||
intake-emails.actions.manage-origins: Zarządzaj dozwolonymi źródłami
|
||||
intake-emails.actions.delete: Usuń
|
||||
intake-emails.delete.confirm.title: Usuąć adres e-mail do przyjęć?
|
||||
intake-emails.delete.confirm.message: Czy na pewno chcesz usunąć ten adres e-mail do przyjęć? Ta akcja jest nieodwracalna.
|
||||
intake-emails.delete.confirm.confirm-button: Usuń adres przyjęć
|
||||
intake-emails.delete.confirm.cancel-button: Anuluj
|
||||
intake-emails.delete.success: Adres przyjęć usunięty
|
||||
intake-emails.create.success: Adres przyjęć utworzony
|
||||
intake-emails.update.success.enabled: Adres przyjęć włączony
|
||||
intake-emails.update.success.disabled: Adres przyjęć wyłączony
|
||||
intake-emails.allowed-origins.title: Dozwolone źródła
|
||||
intake-emails.allowed-origins.description: Tylko e-maile wysłane na {{ email }} z tych źródeł będą przetwarzane. Jeśli nie określono źródeł, wszystkie e-maile zostaną odrzucone.
|
||||
intake-emails.allowed-origins.add.label: Dodaj dozwolony adres e-mail
|
||||
intake-emails.allowed-origins.add.placeholder: 'Przykład: ada@papra.app'
|
||||
intake-emails.allowed-origins.add.button: Dodaj
|
||||
intake-emails.allowed-origins.add.error.exists: Ten adres e-mail jest już w dozwolonych źródłach dla tego adresu e-mail do przyjęć.
|
||||
|
||||
# API keys
|
||||
|
||||
api-keys.permissions.documents.title: Dokumenty
|
||||
api-keys.permissions.documents.documents:create: Tworzenie dokumentów
|
||||
api-keys.permissions.documents.documents:read: Odczyt dokumentów
|
||||
api-keys.permissions.documents.documents:update: Aktualizacja dokumentów
|
||||
api-keys.permissions.documents.documents:delete: Usuwanie dokumentów
|
||||
api-keys.permissions.tags.title: Tag
|
||||
api-keys.permissions.tags.tags:create: Tworzenie tagów
|
||||
api-keys.permissions.tags.tags:read: Odczyt tagów
|
||||
api-keys.permissions.tags.tags:update: Aktualizacja tagów
|
||||
api-keys.permissions.tags.tags:delete: Usuwanie tagów
|
||||
api-keys.create.title: Tworzenie klucza API
|
||||
api-keys.create.description: Utwórz nowy klucz API, aby uzyskać dostęp do API Papra.
|
||||
api-keys.create.success: Klucz API został utworzony pomyślnie.
|
||||
api-keys.create.back: Wróć do kluczy API
|
||||
api-keys.create.form.name.label: Nazwa
|
||||
api-keys.create.form.name.placeholder: 'Przykład: Mój klucz API'
|
||||
api-keys.create.form.name.required: Proszę wprowadzić nazwę dla klucza API
|
||||
api-keys.create.form.permissions.label: Uprawnienia
|
||||
api-keys.create.form.permissions.required: Proszę wybrać co najmniej jedno uprawnienie
|
||||
api-keys.create.form.submit: Utwórz klucz API
|
||||
api-keys.create.created.title: Klucz API utworzony
|
||||
api-keys.create.created.description: Klucz API został utworzony pomyślnie. Zapisz go w bezpiecznym miejscu, ponieważ nie będzie wyświetlony ponownie.
|
||||
api-keys.list.title: Klucze API
|
||||
api-keys.list.description: Zarządzaj swoimi kluczami API tutaj.
|
||||
api-keys.list.create: Utwórz klucz API
|
||||
api-keys.list.empty.title: Brak kluczy API
|
||||
api-keys.list.empty.description: Utwórz klucz API, aby uzyskać dostęp do API Papra.
|
||||
api-keys.list.card.last-used: Ostatnie użycie
|
||||
api-keys.list.card.never: Nigdy
|
||||
api-keys.list.card.created: Utworzono
|
||||
api-keys.delete.success: Klucz API został usunięty pomyślnie
|
||||
api-keys.delete.confirm.title: Usuń klucz API
|
||||
api-keys.delete.confirm.message: Czy na pewno chcesz usunąć ten klucz API? Ta akcja jest nieodwracalna.
|
||||
api-keys.delete.confirm.confirm-button: Usuń
|
||||
api-keys.delete.confirm.cancel-button: Anuluj
|
||||
|
||||
# Webhooks
|
||||
|
||||
webhooks.list.title: Webhooki
|
||||
webhooks.list.description: Zarządzaj webhookami swojej organizacji
|
||||
webhooks.list.empty.title: Brak webhooków
|
||||
webhooks.list.empty.description: Utwórz pierwszy webhook, aby rozpocząć odbieranie zdarzeń
|
||||
webhooks.list.create: Utwórz webhook
|
||||
webhooks.list.card.last-triggered: Ostatnie wywołanie
|
||||
webhooks.list.card.never: Nigdy
|
||||
webhooks.list.card.created: Utworzono
|
||||
webhooks.create.title: Utwórz webhook
|
||||
webhooks.create.description: Utwórz nowy webhook, aby odbierać zdarzenia
|
||||
webhooks.create.success: Webhook został utworzony pomyślnie
|
||||
webhooks.create.back: Wróć
|
||||
webhooks.create.form.submit: Utwórz webhook
|
||||
webhooks.create.form.name.label: Nazwa webhooka
|
||||
webhooks.create.form.name.placeholder: Wprowadź nazwę webhooka
|
||||
webhooks.create.form.name.required: Nazwa jest wymagana
|
||||
webhooks.create.form.url.label: URL webhooka
|
||||
webhooks.create.form.url.placeholder: Wprowadź URL webhooka
|
||||
webhooks.create.form.url.required: URL jest wymagany
|
||||
webhooks.create.form.url.invalid: URL jest nieprawidłowy
|
||||
webhooks.create.form.secret.label: Sekret
|
||||
webhooks.create.form.secret.placeholder: Wprowadź sekret webhooka
|
||||
webhooks.create.form.events.label: Zdarzenia
|
||||
webhooks.create.form.events.required: Co najmniej jedno zdarzenie jest wymagane
|
||||
webhooks.update.title: Edytuj webhook
|
||||
webhooks.update.description: Zaktualizuj szczegóły webhooka
|
||||
webhooks.update.success: Webhook został zaktualizowany pomyślnie
|
||||
webhooks.update.submit: Zaktualizuj webhook
|
||||
webhooks.update.cancel: Anuluj
|
||||
webhooks.update.form.secret.placeholder: Wprowadź nowy sekret
|
||||
webhooks.update.form.secret.placeholder-redacted: '[Zredagowany sekret]'
|
||||
webhooks.update.form.rotate-secret.button: Wygeneruj nowy sekret
|
||||
webhooks.delete.success: Webhook został usunięty pomyślnie
|
||||
webhooks.delete.confirm.title: Usuń webhook
|
||||
webhooks.delete.confirm.message: Czy na pewno chcesz usunąć ten webhook?
|
||||
webhooks.delete.confirm.confirm-button: Usuń
|
||||
webhooks.delete.confirm.cancel-button: Anuluj
|
||||
|
||||
webhooks.events.documents.document:created.description: Utworzono dokument
|
||||
webhooks.events.documents.document:deleted.description: Usunięto dokument
|
||||
|
||||
# Navigation
|
||||
|
||||
layout.menu.home: Strona główna
|
||||
layout.menu.documents: Dokumenty
|
||||
layout.menu.tags: Tagi
|
||||
layout.menu.tagging-rules: Zasady tagowania
|
||||
layout.menu.deleted-documents: Usunięte dokumenty
|
||||
layout.menu.organization-settings: Ustawienia
|
||||
layout.menu.api-keys: Klucze API
|
||||
layout.menu.settings: Ustawienia
|
||||
layout.menu.account: Konto
|
||||
layout.menu.general-settings: Ustawienia ogólne
|
||||
layout.menu.intake-emails: Adresy przyjęć
|
||||
layout.menu.webhooks: Webhooki
|
||||
layout.menu.members: Członkowie
|
||||
layout.menu.invitations: Zaproszenia
|
||||
|
||||
layout.theme.light: Tryb jasny
|
||||
layout.theme.dark: Tryb ciemny
|
||||
layout.theme.system: Tryb systemowy
|
||||
|
||||
layout.search.placeholder: Szukaj...
|
||||
layout.menu.import-document: Importuj dokument
|
||||
|
||||
user-menu.account-settings: Ustawienia konta
|
||||
user-menu.api-keys: Klucze API
|
||||
user-menu.invitations: Zaproszenia
|
||||
user-menu.language: Język
|
||||
user-menu.logout: Wyloguj
|
||||
|
||||
# Command palette
|
||||
|
||||
command-palette.search.placeholder: Szukaj poleceń lub dokumentów
|
||||
command-palette.no-results: Nie znaleziono wyników
|
||||
command-palette.sections.documents: Dokumenty
|
||||
command-palette.sections.theme: Motyw
|
||||
|
||||
# API errors
|
||||
|
||||
api-errors.document.already_exists: Dokument już istnieje
|
||||
api-errors.document.file_too_big: Plik dokumentu jest zbyt duży
|
||||
api-errors.intake_email.limit_reached: Osiągnięto maksymalną liczbę adresów e-mail do przyjęć dla tej organizacji. Aby utworzyć więcej adresów e-mail do przyjęć, zaktualizuj swój plan.
|
||||
api-errors.user.max_organization_count_reached: Osiągnięto maksymalną liczbę organizacji, które możesz utworzyć. Jeśli potrzebujesz utworzyć więcej, skontaktuj się z pomocą techniczną.
|
||||
api-errors.default: Wystąpił błąd podczas przetwarzania żądania.
|
||||
api-errors.organization.invitation_already_exists: Zaproszenie dla tego adresu e-mail już istnieje w tej organizacji.
|
||||
api-errors.user.already_in_organization: Ten użytkownik należy już do tej organizacji.
|
||||
api-errors.user.organization_invitation_limit_reached: Osiągnięto maksymalną liczbę zaproszeń na dzisiaj. Spróbuj ponownie jutro.
|
||||
api-errors.demo.not_available: Ta funkcja nie jest dostępna w wersji demo
|
||||
api-errors.tags.already_exists: Tag o tej nazwie już istnieje w tej organizacji
|
||||
|
||||
# Not found
|
||||
|
||||
not-found.title: 404 - Nie znaleziono
|
||||
not-found.description: Przepraszamy, strona, której szukasz wydaje się nie istnieć. Sprawdź URL i spróbuj ponownie.
|
||||
not-found.back-to-home: Wróć do strony głównej
|
||||
|
||||
# Demo
|
||||
|
||||
demo.popup.description: To jest środowisko demonstracyjne, wszystkie dane są zapisywane w lokalnej pamięci przeglądarki.
|
||||
demo.popup.discord: Dołącz do {{ discordLink }}, aby uzyskać wsparcie, zaproponować funkcje lub po prostu porozmawiać.
|
||||
demo.popup.discord-link-label: Serwer Discord
|
||||
demo.popup.reset: Zresetuj dane demonstracyjne
|
||||
demo.popup.hide: Ukryj
|
||||
|
||||
# Color picker
|
||||
|
||||
color-picker.hue: Odcień
|
||||
color-picker.saturation: Nasycenie
|
||||
color-picker.lightness: Jasność
|
||||
color-picker.select-color: Wybierz kolor
|
||||
color-picker.select-a-color: Wybierz kolor
|
||||
565
apps/papra-client/src/locales/pt-BR.yml
Normal file
565
apps/papra-client/src/locales/pt-BR.yml
Normal file
@@ -0,0 +1,565 @@
|
||||
# Authentication
|
||||
|
||||
auth.request-password-reset.title: Redefina sua senha
|
||||
auth.request-password-reset.description: Insira seu e-mail para redefinir sua senha.
|
||||
auth.request-password-reset.requested: Se uma conta com este e-mail existe, enviamos uma mensagem para redefinir sua senha.
|
||||
auth.request-password-reset.back-to-login: Voltar para o login
|
||||
auth.request-password-reset.form.email.label: E-mail
|
||||
auth.request-password-reset.form.email.placeholder: 'Exemplo: cesar@papra.app'
|
||||
auth.request-password-reset.form.email.required: Por favor, insira seu endereço de e-mail
|
||||
auth.request-password-reset.form.email.invalid: Este endereço de e-mail é inválido
|
||||
auth.request-password-reset.form.submit: Solicitar redefinição de senha
|
||||
|
||||
auth.reset-password.title: Redefina sua senha
|
||||
auth.reset-password.description: Insira sua nova senha para redefinir sua senha.
|
||||
auth.reset-password.reset: Sua senha foi redefinida.
|
||||
auth.reset-password.back-to-login: Voltar para o login
|
||||
auth.reset-password.form.new-password.label: Nova senha
|
||||
auth.reset-password.form.new-password.placeholder: 'Exemplo: **********'
|
||||
auth.reset-password.form.new-password.required: Por favor, insira sua nova senha
|
||||
auth.reset-password.form.new-password.min-length: A senha deve ter pelo menos {{ minLength }} caracteres
|
||||
auth.reset-password.form.new-password.max-length: A senha deve ter menos de {{ maxLength }} caracteres
|
||||
auth.reset-password.form.submit: Redefinir senha
|
||||
|
||||
auth.email-provider.open: Abrir {{ provider }}
|
||||
|
||||
auth.login.title: Acessar o Papra
|
||||
auth.login.description: Insira seu e-mail ou use um login de rede social para acessar sua conta no Papra.
|
||||
auth.login.login-with-provider: Entrar com {{ provider }}
|
||||
auth.login.no-account: Não tem uma conta?
|
||||
auth.login.register: Cadastre-se
|
||||
auth.login.form.email.label: E-mail
|
||||
auth.login.form.email.placeholder: 'Exemplo: cesar@papra.app'
|
||||
auth.login.form.email.required: Por favor, insira seu endereço de e-mail
|
||||
auth.login.form.email.invalid: Este endereço de e-mail é inválido
|
||||
auth.login.form.password.label: Senha
|
||||
auth.login.form.password.placeholder: Defina uma senha
|
||||
auth.login.form.password.required: Por favor, insira sua senha
|
||||
auth.login.form.remember-me.label: Lembrar de mim
|
||||
auth.login.form.forgot-password.label: Esqueceu a senha?
|
||||
auth.login.form.submit: Entrar
|
||||
|
||||
auth.register.title: Cadastre-se no Papra
|
||||
auth.register.description: Crie uma conta para começar a usar o Papra.
|
||||
auth.register.register-with-email: Cadastrar com e-mail
|
||||
auth.register.register-with-provider: Cadastrar com {{ provider }}
|
||||
auth.register.providers.google: Google
|
||||
auth.register.providers.github: GitHub
|
||||
auth.register.have-account: Já tem uma conta?
|
||||
auth.register.login: Entrar
|
||||
auth.register.registration-disabled.title: Cadastro desativado
|
||||
auth.register.registration-disabled.description: A criação de novas contas está desativada nesta instância do Papra. Somente usuários com contas existentes podem acessar. Se você acha que isso é um engano, entre em contato com o administrador desta instância.
|
||||
auth.register.form.email.label: E-mail
|
||||
auth.register.form.email.placeholder: 'Exemplo: cesar@papra.app'
|
||||
auth.register.form.email.required: Por favor, insira seu endereço de e-mail
|
||||
auth.register.form.email.invalid: Este endereço de e-mail é inválido
|
||||
auth.register.form.password.label: Senha
|
||||
auth.register.form.password.placeholder: Defina uma senha
|
||||
auth.register.form.password.required: Por favor, insira sua senha
|
||||
auth.register.form.password.min-length: A senha deve ter pelo menos {{ minLength }} caracteres
|
||||
auth.register.form.password.max-length: A senha deve ter menos de {{ maxLength }} caracteres
|
||||
auth.register.form.name.label: Nome
|
||||
auth.register.form.name.placeholder: 'Exemplo: César Lattes'
|
||||
auth.register.form.name.required: Por favor, insira seu nome
|
||||
auth.register.form.name.max-length: O nome deve ter menos de {{ maxLength }} caracteres
|
||||
auth.register.form.submit: Cadastrar
|
||||
|
||||
auth.email-validation-required.title: Verifique seu e-mail
|
||||
auth.email-validation-required.description: Um e-mail de verificação foi enviado para seu endereço. Por favor, verifique seu e-mail clicando no link enviado.
|
||||
|
||||
auth.legal-links.description: Ao continuar, você reconhece que leu e concorda com os {{ terms }} e a {{ privacy }}.
|
||||
auth.legal-links.terms: Termos de Serviço
|
||||
auth.legal-links.privacy: Política de Privacidade
|
||||
|
||||
auth.no-auth-provider.title: Nenhum provedor de autenticação
|
||||
auth.no-auth-provider.description: Não há provedores de autenticação habilitados nesta instância do Papra. Por favor, entre em contato com o administrador desta instância para habilitá-los.
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: Configurações do usuário
|
||||
user.settings.description: Gerencie as configurações da sua conta aqui.
|
||||
|
||||
user.settings.email.title: Endereço de e-mail
|
||||
user.settings.email.description: Seu endereço de e-mail não pode ser alterado.
|
||||
user.settings.email.label: Endereço de e-mail
|
||||
|
||||
user.settings.name.title: Nome completo
|
||||
user.settings.name.description: Seu nome completo é exibido para outros membros da organização.
|
||||
user.settings.name.label: Nome completo
|
||||
user.settings.name.placeholder: 'Ex: João da Silva'
|
||||
user.settings.name.update: Atualizar nome
|
||||
user.settings.name.updated: Seu nome completo foi atualizado
|
||||
|
||||
user.settings.logout.title: Sair
|
||||
user.settings.logout.description: Encerre a sessão da sua conta. Você poderá acessá-la novamente mais tarde.
|
||||
user.settings.logout.button: Sair
|
||||
|
||||
# Organizations
|
||||
|
||||
organizations.list.title: Suas organizações
|
||||
organizations.list.description: Organizações são uma forma de agrupar seus documentos e gerenciar o acesso a eles. Você pode criar várias organizações e convidar membros da sua equipe para colaborar.
|
||||
organizations.list.create-new: Criar nova organização
|
||||
|
||||
organizations.details.no-documents.title: Nenhum documento
|
||||
organizations.details.no-documents.description: Ainda não há documentos nesta organização. Comece enviando documentos.
|
||||
organizations.details.upload-documents: Enviar documentos
|
||||
organizations.details.documents-count: documentos no total
|
||||
organizations.details.total-size: tamanho total
|
||||
organizations.details.latest-documents: Documentos importados recentemente
|
||||
|
||||
organizations.create.title: Criar uma nova organização
|
||||
organizations.create.description: Seus documentos serão agrupados por organização. Você pode criar várias organizações para separar, por exemplo, documentos pessoais e profissionais.
|
||||
organizations.create.back: Voltar
|
||||
organizations.create.error.max-count-reached: Você atingiu o número máximo de organizações que pode criar. Se precisar criar mais, entre em contato com o suporte.
|
||||
organizations.create.form.name.label: Nome da organização
|
||||
organizations.create.form.name.placeholder: 'Ex: Empresa Ltda.'
|
||||
organizations.create.form.name.required: Por favor, insira um nome para a organização
|
||||
organizations.create.form.submit: Criar organização
|
||||
organizations.create.success: Organização criada com sucesso
|
||||
|
||||
organizations.create-first.title: Crie sua organização
|
||||
organizations.create-first.description: Seus documentos serão agrupados por organização. Você pode criar várias organizações para separar, por exemplo, documentos pessoais e profissionais.
|
||||
organizations.create-first.default-name: Minha organização
|
||||
organizations.create-first.user-name: Organização de {{ name }}
|
||||
|
||||
organization.settings.title: Configurações da Organização
|
||||
organization.settings.page.title: Configurações da organização
|
||||
organization.settings.page.description: Gerencie aqui as configurações da sua organização.
|
||||
organization.settings.name.title: Nome da organização
|
||||
organization.settings.name.update: Atualizar nome
|
||||
organization.settings.name.placeholder: 'Ex: Empresa Ltda.'
|
||||
organization.settings.name.updated: Nome da organização atualizado
|
||||
organization.settings.subscription.title: Assinatura
|
||||
organization.settings.subscription.description: Gerencie sua cobrança, faturas e formas de pagamento.
|
||||
organization.settings.subscription.manage: Gerenciar assinatura
|
||||
organization.settings.subscription.error: Falha ao obter o link do portal do cliente
|
||||
organization.settings.delete.title: Excluir organização
|
||||
organization.settings.delete.description: A exclusão desta organização removerá permanentemente todos seus dados associados.
|
||||
organization.settings.delete.confirm.title: Excluir organização
|
||||
organization.settings.delete.confirm.message: Tem certeza de que deseja excluir esta organização? Esta ação não pode ser desfeita e todos os dados associados serão permanentemente removidos.
|
||||
organization.settings.delete.confirm.confirm-button: Excluir organização
|
||||
organization.settings.delete.confirm.cancel-button: Cancelar
|
||||
organization.settings.delete.success: Organização excluída
|
||||
|
||||
organizations.members.title: Membros
|
||||
organizations.members.description: Gerencie os membros da sua organização
|
||||
organizations.members.invite-member: Convidar membro
|
||||
organizations.members.invite-member-disabled-tooltip: Apenas administradores ou proprietários podem convidar membros para a organização
|
||||
organizations.members.remove-from-organization: Remover da organização
|
||||
organizations.members.role: Função
|
||||
organizations.members.roles.owner: Proprietário
|
||||
organizations.members.roles.admin: Administrador
|
||||
organizations.members.roles.member: Membro
|
||||
organizations.members.delete.confirm.title: Remover membro
|
||||
organizations.members.delete.confirm.message: Tem certeza de que deseja remover este membro da organização?
|
||||
organizations.members.delete.confirm.confirm-button: Remover
|
||||
organizations.members.delete.confirm.cancel-button: Cancelar
|
||||
organizations.members.delete.success: Membro removido da organização
|
||||
organizations.members.update-role.success: Função do membro atualizada
|
||||
organizations.members.table.headers.name: Nome
|
||||
organizations.members.table.headers.email: E-mail
|
||||
organizations.members.table.headers.role: Função
|
||||
organizations.members.table.headers.created: Criado em
|
||||
organizations.members.table.headers.actions: Ações
|
||||
|
||||
organizations.invite-member.title: Convidar membro
|
||||
organizations.invite-member.description: Convide um membro para a sua organização
|
||||
organizations.invite-member.form.email.label: E-mail
|
||||
organizations.invite-member.form.email.placeholder: 'Exemplo: cesar@papra.app'
|
||||
organizations.invite-member.form.email.required: Por favor, insira um endereço de e-mail válido
|
||||
organizations.invite-member.form.role.label: Função
|
||||
organizations.invite-member.form.submit: Convidar para a organização
|
||||
organizations.invite-member.success.message: Membro convidado
|
||||
organizations.invite-member.success.description: O e-mail foi convidado para a organização.
|
||||
organizations.invite-member.error.message: Falha ao convidar o membro
|
||||
|
||||
organizations.invitations.title: Convites
|
||||
organizations.invitations.description: Gerencie os convites da sua organização
|
||||
organizations.invitations.list.cta: Convidar membro
|
||||
organizations.invitations.list.empty.title: Nenhum convite pendente
|
||||
organizations.invitations.list.empty.description: Você ainda não foi convidado para nenhuma organização.
|
||||
organizations.invitations.status.pending: Pendente
|
||||
organizations.invitations.status.accepted: Aceito
|
||||
organizations.invitations.status.rejected: Rejeitado
|
||||
organizations.invitations.status.expired: Expirado
|
||||
organizations.invitations.status.cancelled: Cancelado
|
||||
organizations.invitations.resend: Reenviar convite
|
||||
organizations.invitations.cancel.title: Cancelar convite
|
||||
organizations.invitations.cancel.description: Tem certeza de que deseja cancelar este convite?
|
||||
organizations.invitations.cancel.confirm: Cancelar convite
|
||||
organizations.invitations.cancel.cancel: Cancelar
|
||||
organizations.invitations.resend.title: Reenviar convite
|
||||
organizations.invitations.resend.description: Tem certeza de que deseja reenviar este convite? Um novo e-mail será enviado ao destinatário.
|
||||
organizations.invitations.resend.confirm: Reenviar convite
|
||||
organizations.invitations.resend.cancel: Cancelar
|
||||
|
||||
invitations.list.title: Convites
|
||||
invitations.list.description: Gerencie os convites da sua organização
|
||||
invitations.list.empty.title: Nenhum convite pendente
|
||||
invitations.list.empty.description: Você ainda não foi convidado para nenhuma organização.
|
||||
invitations.list.headers.organization: Organização
|
||||
invitations.list.headers.status: Status
|
||||
invitations.list.headers.created: Criado em
|
||||
invitations.list.headers.actions: Ações
|
||||
invitations.list.actions.accept: Aceitar
|
||||
invitations.list.actions.reject: Rejeitar
|
||||
invitations.list.actions.accept.success.message: Convite aceito
|
||||
invitations.list.actions.accept.success.description: O convite foi aceito.
|
||||
invitations.list.actions.reject.success.message: Convite rejeitado
|
||||
invitations.list.actions.reject.success.description: O convite foi rejeitado.
|
||||
|
||||
# Documents
|
||||
|
||||
documents.list.title: Documentos
|
||||
documents.list.no-documents.title: Nenhum documento
|
||||
documents.list.no-documents.description: Ainda não há documentos nesta organização. Comece enviando documentos.
|
||||
documents.list.no-results: Nenhum documento encontrado
|
||||
|
||||
documents.tabs.info: Informações
|
||||
documents.tabs.content: Conteúdo
|
||||
documents.tabs.activity: Atividades
|
||||
documents.deleted.message: Este documento foi excluído e será deletado permanentemente em {{ days }} dias.
|
||||
documents.actions.download: Baixar
|
||||
documents.actions.open-in-new-tab: Abrir em nova aba
|
||||
documents.actions.restore: Restaurar
|
||||
documents.actions.delete: Excluir
|
||||
documents.actions.edit: Editar
|
||||
documents.actions.cancel: Cancelar
|
||||
documents.actions.save: Salvar
|
||||
documents.actions.saving: Salvando...
|
||||
documents.content.alert: O conteúdo do documento é extraído automaticamente durante o envio e será utilizado apenas para fins de busca e indexação.
|
||||
documents.info.id: ID
|
||||
documents.info.name: Nome
|
||||
documents.info.type: Tipo
|
||||
documents.info.size: Tamanho
|
||||
documents.info.created-at: Criado em
|
||||
documents.info.updated-at: Atualizado em
|
||||
documents.info.never: Nunca
|
||||
|
||||
documents.rename.title: Renomear documento
|
||||
documents.rename.form.name.label: Nome
|
||||
documents.rename.form.name.placeholder: 'Exemplo: Fatura 2024'
|
||||
documents.rename.form.name.required: Por favor, insira um nome para o documento
|
||||
documents.rename.form.name.max-length: O nome deve ter menos de 255 caracteres
|
||||
documents.rename.form.submit: Renomear documento
|
||||
documents.rename.success: Documento renomeado com sucesso
|
||||
documents.rename.cancel: Cancelar
|
||||
|
||||
import-documents.title.error: '{{ count }} documentos falharam'
|
||||
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: Nenhuma importação de documentos em andamento
|
||||
|
||||
documents.deleted.title: Documentos excluídos
|
||||
documents.deleted.empty.title: Nenhum documento excluído
|
||||
documents.deleted.empty.description: Você não tem documentos excluídos. Documentos excluídos serão movidos para a lixeira por {{ days }} dias.
|
||||
documents.deleted.retention-notice: Todos os documentos excluídos são armazenados na lixeira por {{ days }} dias. Após esse período, os documentos serão excluídos permanentemente e não será possível restaurá-los.
|
||||
documents.deleted.deleted-at: Excluído em
|
||||
documents.deleted.restoring: Restaurando...
|
||||
documents.deleted.deleting: Excluindo...
|
||||
|
||||
documents.preview.unknown-file-type: Pré-visualização não disponível para este tipo de arquivo
|
||||
documents.preview.binary-file: Arquivos binários não podem ser exibidos como texto
|
||||
|
||||
trash.delete-all.button: Excluir tudo
|
||||
trash.delete-all.confirm.title: Excluir todos os documentos permanentemente?
|
||||
trash.delete-all.confirm.description: Tem certeza de que deseja excluir permanentemente todos os documentos da lixeira? Esta ação não poderá ser desfeita.
|
||||
trash.delete-all.confirm.label: Excluir
|
||||
trash.delete-all.confirm.cancel: Cancelar
|
||||
trash.delete.button: Excluir
|
||||
trash.delete.confirm.title: Excluir documento permanentemente?
|
||||
trash.delete.confirm.description: Tem certeza de que deseja excluir permanentemente este documento da lixeira? Esta ação não poderá ser desfeita.
|
||||
trash.delete.confirm.label: Excluir
|
||||
trash.delete.confirm.cancel: Cancelar
|
||||
trash.deleted.success.title: Documento excluído
|
||||
trash.deleted.success.description: O documento foi excluído permanentemente.
|
||||
|
||||
activity.document.created: O documento foi criado
|
||||
activity.document.updated.single: O {{ field }} foi atualizado
|
||||
activity.document.updated.multiple: Os {{ fields }} foram atualizados
|
||||
activity.document.updated: O documento foi atualizado
|
||||
activity.document.deleted: O documento foi excluído
|
||||
activity.document.restored: O documento foi restaurado
|
||||
activity.document.tagged: A tag {{ tag }} foi adicionada
|
||||
activity.document.untagged: A tag {{ tag }} foi removida
|
||||
|
||||
activity.document.user.name: por {{ name }}
|
||||
|
||||
activity.load-more: Carregar mais
|
||||
activity.no-more-activities: Não há mais atividades para este documento
|
||||
|
||||
# Tags
|
||||
|
||||
tags.no-tags.title: Nenhuma tag
|
||||
tags.no-tags.description: Esta organização ainda não possui tags. As tags são usadas para categorizar documentos. Você pode adicioná-las aos seus documentos para facilitar a busca e a organização.
|
||||
tags.no-tags.create-tag: Criar tag
|
||||
|
||||
tags.title: Tags de documentos
|
||||
tags.description: As tags são usadas para categorizar documentos. Você pode adicioná-las aos seus documentos para facilitar a busca e a organização.
|
||||
tags.create: Criar tag
|
||||
tags.update: Atualizar tag
|
||||
tags.delete: Excluir tag
|
||||
tags.delete.confirm.title: Excluir tag
|
||||
tags.delete.confirm.message: Tem certeza de que deseja excluir esta tag? A exclusão de uma tag a removerá de todos os documentos.
|
||||
tags.delete.confirm.confirm-button: Excluir
|
||||
tags.delete.confirm.cancel-button: Cancelar
|
||||
tags.delete.success: Tag excluída com sucesso
|
||||
tags.create.success: Tag "{{ name }}" criada com sucesso.
|
||||
tags.update.success: Tag "{{ name }}" atualizada com sucesso.
|
||||
tags.form.name.label: Nome
|
||||
tags.form.name.placeholder: 'Ex: Contratos'
|
||||
tags.form.name.required: Por favor, insira um nome para a tag
|
||||
tags.form.name.max-length: O nome da tag deve ter menos de 64 caracteres
|
||||
tags.form.color.label: Cor
|
||||
tags.form.color.required: Por favor, insira uma cor
|
||||
tags.form.color.invalid: Código hexadecimal formatado incorretamente.
|
||||
tags.form.description.label: Descrição
|
||||
tags.form.description.optional: (opcional)
|
||||
tags.form.description.placeholder: 'Ex: Todos os contratos assinados pela empresa'
|
||||
tags.form.description.max-length: A descrição deve ter menos de 256 caracteres
|
||||
tags.form.no-description: Sem descrição
|
||||
tags.table.headers.tag: Tag
|
||||
tags.table.headers.description: Descrição
|
||||
tags.table.headers.documents: Documentos
|
||||
tags.table.headers.created: Criado em
|
||||
tags.table.headers.actions: Ações
|
||||
|
||||
# Tagging rules
|
||||
|
||||
tagging-rules.field.name: nome do documento
|
||||
tagging-rules.field.content: conteúdo do documento
|
||||
tagging-rules.operator.equals: é igual a
|
||||
tagging-rules.operator.not-equals: é diferente de
|
||||
tagging-rules.operator.contains: contém
|
||||
tagging-rules.operator.not-contains: não contém
|
||||
tagging-rules.operator.starts-with: começa com
|
||||
tagging-rules.operator.ends-with: termina com
|
||||
tagging-rules.list.title: Regras de marcação
|
||||
tagging-rules.list.description: Gerencie as regras de marcação da sua organização para aplicar tags automaticamente a documentos com base em condições definidas por você.
|
||||
tagging-rules.list.demo-warning: 'Nota: Como este é um ambiente de demonstração (sem servidor), as regras de marcação não serão aplicadas a novos documentos adicionados.'
|
||||
tagging-rules.list.no-tagging-rules.title: Nenhuma regra de marcação
|
||||
tagging-rules.list.no-tagging-rules.description: Crie uma regra de marcação para aplicar tags automaticamente aos documentos adicionados, com base em condições definidas por você.
|
||||
tagging-rules.list.no-tagging-rules.create-tagging-rule: Criar regra de marcação
|
||||
tagging-rules.list.card.no-conditions: Nenhuma condição
|
||||
tagging-rules.list.card.one-condition: 1 condição
|
||||
tagging-rules.list.card.conditions: '{{ count }} condições'
|
||||
tagging-rules.list.card.delete: Excluir regra
|
||||
tagging-rules.list.card.edit: Editar regra
|
||||
tagging-rules.create.title: Criar regra de marcação
|
||||
tagging-rules.create.success: Regra de marcação criada com sucesso
|
||||
tagging-rules.create.error: Falha ao criar a regra de marcação
|
||||
tagging-rules.create.submit: Criar regra
|
||||
tagging-rules.form.name.label: Nome
|
||||
tagging-rules.form.name.placeholder: 'Exemplo: Marcar faturas'
|
||||
tagging-rules.form.name.min-length: Por favor, insira um nome para a regra
|
||||
tagging-rules.form.name.max-length: O nome deve ter menos de 64 caracteres
|
||||
tagging-rules.form.description.label: Descrição
|
||||
tagging-rules.form.description.placeholder: "Exemplo: Marcar documentos com 'fatura' no nome"
|
||||
tagging-rules.form.description.max-length: A descrição deve ter menos de 256 caracteres
|
||||
tagging-rules.form.conditions.label: Condições
|
||||
tagging-rules.form.conditions.description: Defina as condições que devem ser atendidas para que a regra seja aplicada. Todas as condições devem ser atendidas.
|
||||
tagging-rules.form.conditions.add-condition: Adicionar condição
|
||||
tagging-rules.form.conditions.no-conditions.title: Nenhuma condição
|
||||
tagging-rules.form.conditions.no-conditions.description: Você não adicionou nenhuma condição a esta regra. Ela será aplicada a todos os documentos.
|
||||
tagging-rules.form.conditions.no-conditions.confirm: Aplicar regra sem condições
|
||||
tagging-rules.form.conditions.no-conditions.cancel: Cancelar
|
||||
tagging-rules.form.conditions.value.placeholder: 'Exemplo: fatura'
|
||||
tagging-rules.form.conditions.value.min-length: Por favor, insira um valor para a condição
|
||||
tagging-rules.form.tags.label: Tags
|
||||
tagging-rules.form.tags.description: Selecione as tags que serão aplicadas aos documentos adicionados que correspondam às condições
|
||||
tagging-rules.form.tags.min-length: Ao menos uma tag para aplicar é necessária
|
||||
tagging-rules.form.tags.add-tag: Criar tag
|
||||
tagging-rules.form.submit: Criar regra
|
||||
tagging-rules.update.title: Atualizar regra de marcação
|
||||
tagging-rules.update.error: Falha ao atualizar a regra de marcação
|
||||
tagging-rules.update.submit: Atualizar regra
|
||||
tagging-rules.update.cancel: Cancelar
|
||||
|
||||
# Intake emails
|
||||
|
||||
intake-emails.title: E-mails de entrada
|
||||
intake-emails.description: Os endereços de e-mail de entrada são usados para importar automaticamente e-mails para o Papra. Basta encaminhar e-mails para o endereço de entrada e os anexos serão adicionados aos documentos da sua organização.
|
||||
intake-emails.disabled.title: E-mails de entrada desativados
|
||||
intake-emails.disabled.description: Os e-mails de entrada estão desativados nesta instância. Por favor, entre em contato com o administrador para ativá-los. Consulte a {{ documentation }} para mais informações.
|
||||
intake-emails.disabled.documentation: documentação
|
||||
intake-emails.info: Apenas e-mails de entrada habilitados e provenientes de origens permitidas serão processados. Você pode ativar ou desativar um e-mail de entrada a qualquer momento.
|
||||
intake-emails.empty.title: Nenhum e-mail de entrada
|
||||
intake-emails.empty.description: Gere um endereço de entrada para importar facilmente anexos de e-mails.
|
||||
intake-emails.empty.generate: Gerar e-mail de entrada
|
||||
intake-emails.count: '{{ count }} e-mail{{ plural }} de entrada para esta organização'
|
||||
intake-emails.new: Novo e-mail de entrada
|
||||
intake-emails.disabled-label: (Desativado)
|
||||
intake-emails.no-origins: Nenhuma origem de e-mail permitida
|
||||
intake-emails.allowed-origins: Permitido de {{ count }} endereço{{ plural }}
|
||||
intake-emails.actions.enable: Ativar
|
||||
intake-emails.actions.disable: Desativar
|
||||
intake-emails.actions.manage-origins: Gerenciar endereços de origem
|
||||
intake-emails.actions.delete: Excluir
|
||||
intake-emails.delete.confirm.title: Excluir e-mail de entrada?
|
||||
intake-emails.delete.confirm.message: Tem certeza de que deseja excluir este e-mail de entrada? Esta ação não poderá ser desfeita.
|
||||
intake-emails.delete.confirm.confirm-button: Excluir e-mail de entrada
|
||||
intake-emails.delete.confirm.cancel-button: Cancelar
|
||||
intake-emails.delete.success: E-mail de entrada excluído
|
||||
intake-emails.create.success: E-mail de entrada criado
|
||||
intake-emails.update.success.enabled: E-mail de entrada ativado
|
||||
intake-emails.update.success.disabled: E-mail de entrada desativado
|
||||
intake-emails.allowed-origins.title: Origens permitidas
|
||||
intake-emails.allowed-origins.description: Apenas e-mails enviados para {{ email }} a partir dessas origens serão processados. Se nenhuma origem for especificada, todos os e-mails serão descartados.
|
||||
intake-emails.allowed-origins.add.label: Adicionar e-mail de origem permitida
|
||||
intake-emails.allowed-origins.add.placeholder: 'Ex: ada@papra.app'
|
||||
intake-emails.allowed-origins.add.button: Adicionar
|
||||
intake-emails.allowed-origins.add.error.exists: Este e-mail já está nas origens permitidas para este e-mail de entrada
|
||||
|
||||
# API keys
|
||||
|
||||
api-keys.permissions.documents.title: Documentos
|
||||
api-keys.permissions.documents.documents:create: Criar documentos
|
||||
api-keys.permissions.documents.documents:read: Ler documentos
|
||||
api-keys.permissions.documents.documents:update: Atualizar documentos
|
||||
api-keys.permissions.documents.documents:delete: Excluir documentos
|
||||
api-keys.permissions.tags.title: Tags
|
||||
api-keys.permissions.tags.tags:create: Criar tags
|
||||
api-keys.permissions.tags.tags:read: Ler tags
|
||||
api-keys.permissions.tags.tags:update: Atualizar tags
|
||||
api-keys.permissions.tags.tags:delete: Excluir tags
|
||||
api-keys.create.title: Criar chave de API
|
||||
api-keys.create.description: Crie uma nova chave de API para acessar a API do Papra.
|
||||
api-keys.create.success: A chave de API foi criada com sucesso.
|
||||
api-keys.create.back: Voltar para as chaves de API
|
||||
api-keys.create.form.name.label: Nome
|
||||
api-keys.create.form.name.placeholder: 'Exemplo: Minha chave de API'
|
||||
api-keys.create.form.name.required: Por favor, insira um nome para a chave de API
|
||||
api-keys.create.form.permissions.label: Permissões
|
||||
api-keys.create.form.permissions.required: Por favor, selecione ao menos uma permissão
|
||||
api-keys.create.form.submit: Criar chave de API
|
||||
api-keys.create.created.title: Chave de API criada
|
||||
api-keys.create.created.description: A chave de API foi criada com sucesso. Salve-a em um local seguro, pois ela não será exibida novamente.
|
||||
api-keys.list.title: Chaves de API
|
||||
api-keys.list.description: Gerencie suas chaves de API aqui.
|
||||
api-keys.list.create: Criar chave de API
|
||||
api-keys.list.empty.title: Nenhuma chave de API
|
||||
api-keys.list.empty.description: Crie uma chave de API para acessar a API do Papra.
|
||||
api-keys.list.card.last-used: Último uso
|
||||
api-keys.list.card.never: Nunca
|
||||
api-keys.list.card.created: Criada em
|
||||
api-keys.delete.success: A chave de API foi excluída com sucesso
|
||||
api-keys.delete.confirm.title: Excluir chave de API
|
||||
api-keys.delete.confirm.message: Tem certeza de que deseja excluir esta chave de API? Esta ação não poderá ser desfeita.
|
||||
api-keys.delete.confirm.confirm-button: Excluir
|
||||
api-keys.delete.confirm.cancel-button: Cancelar
|
||||
|
||||
# Webhooks
|
||||
|
||||
webhooks.list.title: Webhooks
|
||||
webhooks.list.description: Gerencie os webhooks da sua organização
|
||||
webhooks.list.empty.title: Nenhum webhook
|
||||
webhooks.list.empty.description: Crie seu primeiro webhook para começar a receber eventos
|
||||
webhooks.list.create: Criar webhook
|
||||
webhooks.list.card.last-triggered: Última ativação
|
||||
webhooks.list.card.never: Nunca
|
||||
webhooks.list.card.created: Criado em
|
||||
webhooks.create.title: Criar webhook
|
||||
webhooks.create.description: Crie um novo webhook para receber eventos
|
||||
webhooks.create.success: Webhook criado com sucesso
|
||||
webhooks.create.back: Voltar
|
||||
webhooks.create.form.submit: Criar webhook
|
||||
webhooks.create.form.name.label: Nome do webhook
|
||||
webhooks.create.form.name.placeholder: Insira o nome do webhook
|
||||
webhooks.create.form.name.required: O nome é obrigatório
|
||||
webhooks.create.form.url.label: URL do Webhook
|
||||
webhooks.create.form.url.placeholder: Insira a URL do webhook
|
||||
webhooks.create.form.url.required: A URL é obrigatória
|
||||
webhooks.create.form.url.invalid: URL inválida
|
||||
webhooks.create.form.secret.label: Segredo
|
||||
webhooks.create.form.secret.placeholder: Insira o segredo do webhook
|
||||
webhooks.create.form.events.label: Eventos
|
||||
webhooks.create.form.events.required: Adicione pelo menos um evento
|
||||
webhooks.update.title: Editar webhook
|
||||
webhooks.update.description: Atualize os detalhes do seu webhook
|
||||
webhooks.update.success: Webhook atualizado com sucesso
|
||||
webhooks.update.submit: Atualizar webhook
|
||||
webhooks.update.cancel: Cancelar
|
||||
webhooks.update.form.secret.placeholder: Insira um novo segredo
|
||||
webhooks.update.form.secret.placeholder-redacted: '[Segredo ocultado]'
|
||||
webhooks.update.form.rotate-secret.button: Rotacionar segredo
|
||||
webhooks.delete.success: Webhook excluído com sucesso
|
||||
webhooks.delete.confirm.title: Excluir webhook
|
||||
webhooks.delete.confirm.message: Tem certeza de que deseja excluir este webhook?
|
||||
webhooks.delete.confirm.confirm-button: Excluir
|
||||
webhooks.delete.confirm.cancel-button: Cancelar
|
||||
|
||||
webhooks.events.documents.document:created.description: Documento criado
|
||||
webhooks.events.documents.document:deleted.description: Documento excluído
|
||||
|
||||
# Navigation
|
||||
|
||||
layout.menu.home: Início
|
||||
layout.menu.documents: Documentos
|
||||
layout.menu.tags: Tags
|
||||
layout.menu.tagging-rules: Regras de marcação
|
||||
layout.menu.deleted-documents: Documentos excluídos
|
||||
layout.menu.organization-settings: Configurações
|
||||
layout.menu.api-keys: Chaves de API
|
||||
layout.menu.settings: Configurações
|
||||
layout.menu.account: Conta
|
||||
layout.menu.general-settings: Configurações gerais
|
||||
layout.menu.intake-emails: E-mails de entrada
|
||||
layout.menu.webhooks: Webhooks
|
||||
layout.menu.members: Membros
|
||||
layout.menu.invitations: Convites
|
||||
|
||||
layout.theme.light: Tema claro
|
||||
layout.theme.dark: Tema escuro
|
||||
layout.theme.system: Tema do sistema
|
||||
|
||||
layout.search.placeholder: Buscar...
|
||||
layout.menu.import-document: Importar um documento
|
||||
|
||||
user-menu.account-settings: Configurações da conta
|
||||
user-menu.api-keys: Chaves de API
|
||||
user-menu.invitations: Convites
|
||||
user-menu.language: Idioma
|
||||
user-menu.logout: Sair
|
||||
|
||||
# Command palette
|
||||
|
||||
command-palette.search.placeholder: Buscar comandos ou documentos
|
||||
command-palette.no-results: Nenhum resultado encontrado
|
||||
command-palette.sections.documents: Documentos
|
||||
command-palette.sections.theme: Tema
|
||||
|
||||
# API errors
|
||||
|
||||
api-errors.document.already_exists: O documento já existe
|
||||
api-errors.document.file_too_big: O arquivo do documento é muito grande
|
||||
api-errors.intake_email.limit_reached: O número máximo de e-mails de entrada para esta organização foi atingido. Faça um upgrade no seu plano para criar mais e-mails de entrada.
|
||||
api-errors.user.max_organization_count_reached: Você atingiu o número máximo de organizações que pode criar. Se precisar criar mais, entre em contato com o suporte.
|
||||
api-errors.default: Ocorreu um erro ao processar sua solicitação.
|
||||
api-errors.organization.invitation_already_exists: Já existe um convite para este e-mail nesta organização.
|
||||
api-errors.user.already_in_organization: Este usuário já faz parte desta organização.
|
||||
api-errors.user.organization_invitation_limit_reached: O número máximo de convites por hoje foi atingido. Por favor, tente novamente amanhã.
|
||||
api-errors.demo.not_available: Este recurso não está disponível em ambiente de demonstração
|
||||
api-errors.tags.already_exists: Já existe uma tag com este nome nesta organização
|
||||
|
||||
# Not found
|
||||
|
||||
not-found.title: 404 - Página não encontrada
|
||||
not-found.description: Desculpe, a página que você está procurando não existe. Verifique o URL e tente novamente.
|
||||
not-found.back-to-home: Voltar para a página inicial
|
||||
|
||||
# Demo
|
||||
|
||||
demo.popup.description: Este é um ambiente de demonstração; todos os dados são salvos no armazenamento local do seu navegador.
|
||||
demo.popup.discord: Entre no {{ discordLink }} para obter suporte, sugerir funcionalidades ou apenas conversar.
|
||||
demo.popup.discord-link-label: Comunidade do Discord
|
||||
demo.popup.reset: Redefinir dados da demonstração
|
||||
demo.popup.hide: Ocultar
|
||||
|
||||
# Color picker
|
||||
|
||||
color-picker.hue: Matiz
|
||||
color-picker.saturation: Saturação
|
||||
color-picker.lightness: Brilho
|
||||
color-picker.select-color: Selecionar cor
|
||||
color-picker.select-a-color: Selecione uma cor
|
||||
562
apps/papra-client/src/locales/pt.yml
Normal file
562
apps/papra-client/src/locales/pt.yml
Normal file
@@ -0,0 +1,562 @@
|
||||
# Authentication
|
||||
|
||||
auth.request-password-reset.title: Redefinir a sua palavra-passe
|
||||
auth.request-password-reset.description: Introduza o seu e-mail para redefinir a palavra-passe.
|
||||
auth.request-password-reset.requested: Se existir uma conta para este e-mail, enviámos-lhe um e-mail para redefinir a palavra-passe.
|
||||
auth.request-password-reset.back-to-login: Voltar ao início de sessão
|
||||
auth.request-password-reset.form.email.label: E-mail
|
||||
auth.request-password-reset.form.email.placeholder: 'Exemplo: joao@papra.app'
|
||||
auth.request-password-reset.form.email.required: Por favor, introduza o seu endereço de e-mail
|
||||
auth.request-password-reset.form.email.invalid: Este endereço de e-mail é inválido
|
||||
auth.request-password-reset.form.submit: Solicitar redefinição de palavra-passe
|
||||
|
||||
auth.reset-password.title: Redefinir a sua palavra-passe
|
||||
auth.reset-password.description: Introduza a sua nova palavra-passe para redefinir a palavra-passe.
|
||||
auth.reset-password.reset: A sua palavra-passe foi redefinida.
|
||||
auth.reset-password.back-to-login: Voltar ao início de sessão
|
||||
auth.reset-password.form.new-password.label: Nova palavra-passe
|
||||
auth.reset-password.form.new-password.placeholder: 'Exemplo: **********'
|
||||
auth.reset-password.form.new-password.required: Por favor, introduza a sua nova palavra-passe
|
||||
auth.reset-password.form.new-password.min-length: A palavra-passe deve ter pelo menos {{ minLength }} caracteres
|
||||
auth.reset-password.form.new-password.max-length: A palavra-passe deve ter menos de {{ maxLength }} caracteres
|
||||
auth.reset-password.form.submit: Redefinir palavra-passe
|
||||
|
||||
auth.email-provider.open: Abrir {{ provider }}
|
||||
|
||||
auth.login.title: Iniciar sessão no Papra
|
||||
auth.login.description: Introduza o seu e-mail ou use o início de sessão social para aceder à sua conta Papra.
|
||||
auth.login.login-with-provider: Iniciar sessão com {{ provider }}
|
||||
auth.login.no-account: Não tem uma conta?
|
||||
auth.login.register: Registar
|
||||
auth.login.form.email.label: E-mail
|
||||
auth.login.form.email.placeholder: 'Exemplo: joao@papra.app'
|
||||
auth.login.form.email.required: Por favor, introduza o seu endereço de e-mail
|
||||
auth.login.form.email.invalid: Este endereço de e-mail é inválido
|
||||
auth.login.form.password.label: Palavra-passe
|
||||
auth.login.form.password.placeholder: Definir uma palavra-passe
|
||||
auth.login.form.password.required: Por favor, introduza a sua palavra-passe
|
||||
auth.login.form.remember-me.label: Lembrar-me
|
||||
auth.login.form.forgot-password.label: Esqueceu-se da palavra-passe?
|
||||
auth.login.form.submit: Iniciar sessão
|
||||
|
||||
auth.register.title: Registar no Papra
|
||||
auth.register.description: Crie uma conta para começar a usar o Papra.
|
||||
auth.register.register-with-email: Registar com e-mail
|
||||
auth.register.register-with-provider: Registar com {{ provider }}
|
||||
auth.register.providers.google: Google
|
||||
auth.register.providers.github: GitHub
|
||||
auth.register.have-account: Já tem uma conta?
|
||||
auth.register.login: Iniciar sessão
|
||||
auth.register.registration-disabled.title: O registo está desativado
|
||||
auth.register.registration-disabled.description: A criação de novas contas está atualmente desativada nesta instância do Papra. Apenas utilizadores com contas existentes podem iniciar sessão. Se acha que isto é um erro, contacte o administrador desta instância.
|
||||
auth.register.form.email.label: E-mail
|
||||
auth.register.form.email.placeholder: 'Exemplo: joao@papra.app'
|
||||
auth.register.form.email.required: Por favor, introduza o seu endereço de e-mail
|
||||
auth.register.form.email.invalid: Este endereço de e-mail é inválido
|
||||
auth.register.form.password.label: Palavra-passe
|
||||
auth.register.form.password.placeholder: Definir uma palavra-passe
|
||||
auth.register.form.password.required: Por favor, introduza a sua palavra-passe
|
||||
auth.register.form.password.min-length: A palavra-passe deve ter pelo menos {{ minLength }} caracteres
|
||||
auth.register.form.password.max-length: A palavra-passe deve ter menos de {{ maxLength }} caracteres
|
||||
auth.register.form.name.label: Nome
|
||||
auth.register.form.name.placeholder: 'Exemplo: Ada Lovelace'
|
||||
auth.register.form.name.required: Por favor, introduza o seu nome
|
||||
auth.register.form.name.max-length: O nome deve ter menos de {{ maxLength }} caracteres
|
||||
auth.register.form.submit: Registar
|
||||
|
||||
auth.email-validation-required.title: Verifique o seu e-mail
|
||||
auth.email-validation-required.description: Foi enviado um e-mail de verificação para o seu endereço de e-mail. Por favor, verifique o seu endereço de e-mail clicando na ligação no e-mail.
|
||||
|
||||
auth.legal-links.description: Ao continuar, reconhece que compreende e concorda com os {{ terms }} e a {{ privacy }}.
|
||||
auth.legal-links.terms: Termos de Serviço
|
||||
auth.legal-links.privacy: Política de Privacidade
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: Definições do utilizador
|
||||
user.settings.description: Gira as definições da sua conta aqui.
|
||||
|
||||
user.settings.email.title: Endereço de e-mail
|
||||
user.settings.email.description: O seu endereço de e-mail não pode ser alterado.
|
||||
user.settings.email.label: Endereço de e-mail
|
||||
|
||||
user.settings.name.title: Nome completo
|
||||
user.settings.name.description: O seu nome completo é exibido a outros membros da organização.
|
||||
user.settings.name.label: Nome completo
|
||||
user.settings.name.placeholder: Ex. João Silva
|
||||
user.settings.name.update: Atualizar nome
|
||||
user.settings.name.updated: O seu nome completo foi atualizado
|
||||
|
||||
user.settings.logout.title: Terminar sessão
|
||||
user.settings.logout.description: Terminar sessão da sua conta. Pode iniciar sessão novamente mais tarde.
|
||||
user.settings.logout.button: Terminar sessão
|
||||
|
||||
# Organizations
|
||||
|
||||
organizations.list.title: As suas organizações
|
||||
organizations.list.description: As organizações são uma forma de agrupar os seus documentos e gerir o acesso aos mesmos. Pode criar várias organizações e convidar os membros da sua equipa para colaborar.
|
||||
organizations.list.create-new: Criar nova organização
|
||||
|
||||
organizations.details.no-documents.title: Sem documentos
|
||||
organizations.details.no-documents.description: Não há documentos nesta organização ainda. Comece por carregar alguns documentos.
|
||||
organizations.details.upload-documents: Carregar documentos
|
||||
organizations.details.documents-count: documentos no total
|
||||
organizations.details.total-size: tamanho total
|
||||
organizations.details.latest-documents: Últimos documentos importados
|
||||
|
||||
organizations.create.title: Criar uma nova organização
|
||||
organizations.create.description: Os seus documentos serão agrupados por organização. Pode criar várias organizações para separar os seus documentos, por exemplo, para documentos pessoais e de trabalho.
|
||||
organizations.create.back: Voltar
|
||||
organizations.create.error.max-count-reached: Atingiu o número máximo de organizações que pode criar, se precisar de criar mais, contacte o suporte.
|
||||
organizations.create.form.name.label: Nome da organização
|
||||
organizations.create.form.name.placeholder: Ex. Acme Inc.
|
||||
organizations.create.form.name.required: Por favor, introduza um nome para a organização
|
||||
organizations.create.form.submit: Criar organização
|
||||
organizations.create.success: Organização criada com sucesso
|
||||
|
||||
organizations.create-first.title: Criar a sua organização
|
||||
organizations.create-first.description: Os seus documentos serão agrupados por organização. Pode criar várias organizações para separar os seus documentos, por exemplo, para documentos pessoais e de trabalho.
|
||||
organizations.create-first.default-name: A minha organização
|
||||
organizations.create-first.user-name: 'Organização de {{ name }}'
|
||||
|
||||
organization.settings.title: Definições da Organização
|
||||
organization.settings.page.title: Definições da organização
|
||||
organization.settings.page.description: Gira as definições da sua organização aqui.
|
||||
organization.settings.name.title: Nome da organização
|
||||
organization.settings.name.update: Atualizar nome
|
||||
organization.settings.name.placeholder: Ex. Acme Inc.
|
||||
organization.settings.name.updated: Nome da organização atualizado
|
||||
organization.settings.subscription.title: Subscrição
|
||||
organization.settings.subscription.description: Gira a sua faturação, faturas e métodos de pagamento.
|
||||
organization.settings.subscription.manage: Gerir subscrição
|
||||
organization.settings.subscription.error: Falha ao obter URL do portal do cliente
|
||||
organization.settings.delete.title: Eliminar organização
|
||||
organization.settings.delete.description: Eliminar esta organização removerá permanentemente todos os dados associados à mesma.
|
||||
organization.settings.delete.confirm.title: Eliminar organização
|
||||
organization.settings.delete.confirm.message: Tem a certeza de que quer eliminar esta organização? Esta ação não pode ser desfeita e todos os dados associados a esta organização serão permanentemente removidos.
|
||||
organization.settings.delete.confirm.confirm-button: Eliminar organização
|
||||
organization.settings.delete.confirm.cancel-button: Cancelar
|
||||
organization.settings.delete.success: Organização eliminada
|
||||
|
||||
organizations.members.title: Membros
|
||||
organizations.members.description: Gira os membros da sua organização
|
||||
organizations.members.invite-member: Convidar membro
|
||||
organizations.members.invite-member-disabled-tooltip: Apenas administradores ou proprietários podem convidar membros para a organização
|
||||
organizations.members.remove-from-organization: Remover da organização
|
||||
organizations.members.role: Função
|
||||
organizations.members.roles.owner: Proprietário
|
||||
organizations.members.roles.admin: Administrador
|
||||
organizations.members.roles.member: Membro
|
||||
organizations.members.delete.confirm.title: Remover membro
|
||||
organizations.members.delete.confirm.message: Tem a certeza de que quer remover este membro da organização?
|
||||
organizations.members.delete.confirm.confirm-button: Remover
|
||||
organizations.members.delete.confirm.cancel-button: Cancelar
|
||||
organizations.members.delete.success: Membro removido da organização
|
||||
organizations.members.update-role.success: Função do membro atualizada
|
||||
organizations.members.table.headers.name: Nome
|
||||
organizations.members.table.headers.email: E-mail
|
||||
organizations.members.table.headers.role: Função
|
||||
organizations.members.table.headers.created: Criado
|
||||
organizations.members.table.headers.actions: Ações
|
||||
|
||||
organizations.invite-member.title: Convidar membro
|
||||
organizations.invite-member.description: Convide um membro para a sua organização
|
||||
organizations.invite-member.form.email.label: E-mail
|
||||
organizations.invite-member.form.email.placeholder: 'Exemplo: joao@papra.app'
|
||||
organizations.invite-member.form.email.required: Por favor, introduza um endereço de e-mail válido
|
||||
organizations.invite-member.form.role.label: Função
|
||||
organizations.invite-member.form.submit: Convidar para a organização
|
||||
organizations.invite-member.success.message: Membro convidado
|
||||
organizations.invite-member.success.description: O e-mail foi convidado para a organização.
|
||||
organizations.invite-member.error.message: Falha ao convidar membro
|
||||
|
||||
organizations.invitations.title: Convites
|
||||
organizations.invitations.description: Gira os convites da sua organização
|
||||
organizations.invitations.list.cta: Convidar membro
|
||||
organizations.invitations.list.empty.title: Sem convites pendentes
|
||||
organizations.invitations.list.empty.description: Ainda não foi convidado para nenhuma organização.
|
||||
organizations.invitations.status.pending: Pendente
|
||||
organizations.invitations.status.accepted: Aceite
|
||||
organizations.invitations.status.rejected: Rejeitado
|
||||
organizations.invitations.status.expired: Expirado
|
||||
organizations.invitations.status.cancelled: Cancelado
|
||||
organizations.invitations.resend: Reenviar convite
|
||||
organizations.invitations.cancel.title: Cancelar convite
|
||||
organizations.invitations.cancel.description: Tem a certeza de que quer cancelar este convite?
|
||||
organizations.invitations.cancel.confirm: Cancelar convite
|
||||
organizations.invitations.cancel.cancel: Cancelar
|
||||
organizations.invitations.resend.title: Reenviar convite
|
||||
organizations.invitations.resend.description: Tem a certeza de que quer reenviar este convite? Isto enviará um novo e-mail ao destinatário.
|
||||
organizations.invitations.resend.confirm: Reenviar convite
|
||||
organizations.invitations.resend.cancel: Cancelar
|
||||
|
||||
invitations.list.title: Convites
|
||||
invitations.list.description: Gira os convites da sua organização
|
||||
invitations.list.empty.title: Sem convites pendentes
|
||||
invitations.list.empty.description: Ainda não foi convidado para nenhuma organização.
|
||||
invitations.list.headers.organization: Organização
|
||||
invitations.list.headers.status: Estado
|
||||
invitations.list.headers.created: Criado
|
||||
invitations.list.headers.actions: Ações
|
||||
invitations.list.actions.accept: Aceitar
|
||||
invitations.list.actions.reject: Rejeitar
|
||||
invitations.list.actions.accept.success.message: Convite aceite
|
||||
invitations.list.actions.accept.success.description: O convite foi aceite.
|
||||
invitations.list.actions.reject.success.message: Convite rejeitado
|
||||
invitations.list.actions.reject.success.description: O convite foi rejeitado.
|
||||
|
||||
# Documents
|
||||
|
||||
documents.list.title: Documentos
|
||||
documents.list.no-documents.title: Sem documentos
|
||||
documents.list.no-documents.description: Não há documentos nesta organização ainda. Comece por carregar alguns documentos.
|
||||
documents.list.no-results: Nenhum documento encontrado
|
||||
|
||||
documents.tabs.info: Informação
|
||||
documents.tabs.content: Conteúdo
|
||||
documents.tabs.activity: Atividade
|
||||
documents.deleted.message: Este documento foi eliminado e será permanentemente removido em {{ days }} dias.
|
||||
documents.actions.download: Descarregar
|
||||
documents.actions.open-in-new-tab: Abrir em novo separador
|
||||
documents.actions.restore: Restaurar
|
||||
documents.actions.delete: Eliminar
|
||||
documents.actions.edit: Editar
|
||||
documents.actions.cancel: Cancelar
|
||||
documents.actions.save: Guardar
|
||||
documents.actions.saving: A guardar...
|
||||
documents.content.alert: O conteúdo do documento é automaticamente extraído do documento no carregamento. É usado apenas para fins de pesquisa e indexação.
|
||||
documents.info.id: ID
|
||||
documents.info.name: Nome
|
||||
documents.info.type: Tipo
|
||||
documents.info.size: Tamanho
|
||||
documents.info.created-at: Criado em
|
||||
documents.info.updated-at: Atualizado em
|
||||
documents.info.never: Nunca
|
||||
|
||||
documents.rename.title: Renomear documento
|
||||
documents.rename.form.name.label: Nome
|
||||
documents.rename.form.name.placeholder: 'Exemplo: Fatura 2024'
|
||||
documents.rename.form.name.required: Por favor, introduza um nome para o documento
|
||||
documents.rename.form.name.max-length: O nome deve ter menos de 255 caracteres
|
||||
documents.rename.form.submit: Renomear documento
|
||||
documents.rename.success: Documento renomeado com sucesso
|
||||
documents.rename.cancel: Cancelar
|
||||
|
||||
import-documents.title.error: '{{ count }} documentos falharam'
|
||||
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: Nenhuma importação de documento em progresso
|
||||
|
||||
documents.deleted.title: Documentos eliminados
|
||||
documents.deleted.empty.title: Sem documentos eliminados
|
||||
documents.deleted.empty.description: Não tem documentos eliminados. Os documentos que são eliminados serão movidos para a reciclagem por {{ days }} dias.
|
||||
documents.deleted.retention-notice: Todos os documentos eliminados são armazenados na reciclagem por {{ days }} dias. Passando este prazo, os documentos serão permanentemente eliminados e não poderá restaurá-los.
|
||||
documents.deleted.deleted-at: Eliminado
|
||||
documents.deleted.restoring: A restaurar...
|
||||
documents.deleted.deleting: A eliminar...
|
||||
|
||||
documents.preview.unknown-file-type: Não há pré-visualização disponível para este tipo de ficheiro
|
||||
documents.preview.binary-file: Este parece ser um ficheiro binário e não pode ser exibido como texto
|
||||
|
||||
trash.delete-all.button: Eliminar tudo
|
||||
trash.delete-all.confirm.title: Eliminar permanentemente todos os documentos?
|
||||
trash.delete-all.confirm.description: Tem a certeza de que quer eliminar permanentemente todos os documentos da reciclagem? Esta ação não pode ser desfeita.
|
||||
trash.delete-all.confirm.label: Eliminar
|
||||
trash.delete-all.confirm.cancel: Cancelar
|
||||
trash.delete.button: Eliminar
|
||||
trash.delete.confirm.title: Eliminar documento permanentemente?
|
||||
trash.delete.confirm.description: Tem a certeza de que quer eliminar permanentemente este documento da reciclagem? Esta ação não pode ser desfeita.
|
||||
trash.delete.confirm.label: Eliminar
|
||||
trash.delete.confirm.cancel: Cancelar
|
||||
trash.deleted.success.title: Documento eliminado
|
||||
trash.deleted.success.description: O documento foi eliminado permanentemente.
|
||||
|
||||
activity.document.created: O documento foi criado
|
||||
activity.document.updated.single: O {{ field }} foi atualizado
|
||||
activity.document.updated.multiple: Os {{ fields }} foram atualizados
|
||||
activity.document.updated: O documento foi atualizado
|
||||
activity.document.deleted: O documento foi eliminado
|
||||
activity.document.restored: O documento foi restaurado
|
||||
activity.document.tagged: A etiqueta {{ tag }} foi adicionada
|
||||
activity.document.untagged: A etiqueta {{ tag }} foi removida
|
||||
|
||||
activity.document.user.name: por {{ name }}
|
||||
|
||||
activity.load-more: Carregar mais
|
||||
activity.no-more-activities: Não há mais atividades para este documento
|
||||
|
||||
# Tags
|
||||
|
||||
tags.no-tags.title: Ainda sem etiquetas
|
||||
tags.no-tags.description: Esta organização ainda não tem etiquetas. As etiquetas são usadas para categorizar documentos. Pode adicionar etiquetas aos seus documentos para os tornar mais fáceis de encontrar e organizar.
|
||||
tags.no-tags.create-tag: Criar etiqueta
|
||||
|
||||
tags.title: Etiquetas de Documentos
|
||||
tags.description: As etiquetas são usadas para categorizar documentos. Pode adicionar etiquetas aos seus documentos para os tornar mais fáceis de encontrar e organizar.
|
||||
tags.create: Criar etiqueta
|
||||
tags.update: Atualizar etiqueta
|
||||
tags.delete: Eliminar etiqueta
|
||||
tags.delete.confirm.title: Eliminar etiqueta
|
||||
tags.delete.confirm.message: Tem a certeza de que quer eliminar esta etiqueta? Eliminar uma etiqueta irá removê-la de todos os documentos.
|
||||
tags.delete.confirm.confirm-button: Eliminar
|
||||
tags.delete.confirm.cancel-button: Cancelar
|
||||
tags.delete.success: Etiqueta eliminada com sucesso
|
||||
tags.create.success: Etiqueta "{{ name }}" criada com sucesso.
|
||||
tags.update.success: Etiqueta "{{ name }}" atualizada com sucesso.
|
||||
tags.form.name.label: Nome
|
||||
tags.form.name.placeholder: Ex. Contratos
|
||||
tags.form.name.required: Por favor, introduza um nome para a etiqueta
|
||||
tags.form.name.max-length: O nome da etiqueta deve ter menos de 64 caracteres
|
||||
tags.form.color.label: Cor
|
||||
tags.form.color.required: Por favor, introduza uma cor
|
||||
tags.form.color.invalid: A cor hexadecimal está mal formatada.
|
||||
tags.form.description.label: Descrição
|
||||
tags.form.description.optional: (opcional)
|
||||
tags.form.description.placeholder: Ex. Todos os contratos assinados pela empresa
|
||||
tags.form.description.max-length: A descrição deve ter menos de 256 caracteres
|
||||
tags.form.no-description: Sem descrição
|
||||
tags.table.headers.tag: Etiqueta
|
||||
tags.table.headers.description: Descrição
|
||||
tags.table.headers.documents: Documentos
|
||||
tags.table.headers.created: Criado
|
||||
tags.table.headers.actions: Ações
|
||||
|
||||
# Tagging rules
|
||||
|
||||
tagging-rules.field.name: nome do documento
|
||||
tagging-rules.field.content: conteúdo do documento
|
||||
tagging-rules.operator.equals: igual a
|
||||
tagging-rules.operator.not-equals: não igual a
|
||||
tagging-rules.operator.contains: contém
|
||||
tagging-rules.operator.not-contains: não contém
|
||||
tagging-rules.operator.starts-with: começa com
|
||||
tagging-rules.operator.ends-with: termina com
|
||||
tagging-rules.list.title: Regras de etiquetagem
|
||||
tagging-rules.list.description: Gira as regras de etiquetagem da sua organização, para etiquetar automaticamente documentos com base em condições que define.
|
||||
tagging-rules.list.demo-warning: 'Nota: Como este é um ambiente de demonstração (sem servidor), as regras de etiquetagem não serão aplicadas a documentos recém-adicionados.'
|
||||
tagging-rules.list.no-tagging-rules.title: Sem regras de etiquetagem
|
||||
tagging-rules.list.no-tagging-rules.description: Crie uma regra de etiquetagem para etiquetar automaticamente os seus documentos adicionados com base em condições que define.
|
||||
tagging-rules.list.no-tagging-rules.create-tagging-rule: Criar regra de etiquetagem
|
||||
tagging-rules.list.card.no-conditions: Sem condições
|
||||
tagging-rules.list.card.one-condition: 1 condição
|
||||
tagging-rules.list.card.conditions: '{{ count }} condições'
|
||||
tagging-rules.list.card.delete: Eliminar regra
|
||||
tagging-rules.list.card.edit: Editar regra
|
||||
tagging-rules.create.title: Criar regra de etiquetagem
|
||||
tagging-rules.create.success: Regra de etiquetagem criada com sucesso
|
||||
tagging-rules.create.error: Falha ao criar regra de etiquetagem
|
||||
tagging-rules.create.submit: Criar regra
|
||||
tagging-rules.form.name.label: Nome
|
||||
tagging-rules.form.name.placeholder: 'Exemplo: Etiquetar faturas'
|
||||
tagging-rules.form.name.min-length: Por favor, introduza um nome para a regra
|
||||
tagging-rules.form.name.max-length: O nome deve ter menos de 64 caracteres
|
||||
tagging-rules.form.description.label: Descrição
|
||||
tagging-rules.form.description.placeholder: "Exemplo: Etiquetar documentos com 'fatura' no nome"
|
||||
tagging-rules.form.description.max-length: A descrição deve ter menos de 256 caracteres
|
||||
tagging-rules.form.conditions.label: Condições
|
||||
tagging-rules.form.conditions.description: Defina as condições que devem ser cumpridas para a regra se aplicar. Todas as condições devem ser cumpridas para a regra se aplicar.
|
||||
tagging-rules.form.conditions.add-condition: Adicionar condição
|
||||
tagging-rules.form.conditions.no-conditions.title: Sem condições
|
||||
tagging-rules.form.conditions.no-conditions.description: Não adicionou nenhuma condição a esta regra. Esta regra aplicará as suas etiquetas a todos os documentos.
|
||||
tagging-rules.form.conditions.no-conditions.confirm: Aplicar regra sem condições
|
||||
tagging-rules.form.conditions.no-conditions.cancel: Cancelar
|
||||
tagging-rules.form.conditions.value.placeholder: 'Exemplo: fatura'
|
||||
tagging-rules.form.conditions.value.min-length: Por favor, introduza um valor para a condição
|
||||
tagging-rules.form.tags.label: Etiquetas
|
||||
tagging-rules.form.tags.description: Selecione as etiquetas a aplicar aos documentos adicionados que correspondem às condições
|
||||
tagging-rules.form.tags.min-length: É necessária pelo menos uma etiqueta para aplicar
|
||||
tagging-rules.form.tags.add-tag: Criar etiqueta
|
||||
tagging-rules.form.submit: Criar regra
|
||||
tagging-rules.update.title: Atualizar regra de etiquetagem
|
||||
tagging-rules.update.error: Falha ao atualizar regra de etiquetagem
|
||||
tagging-rules.update.submit: Atualizar regra
|
||||
tagging-rules.update.cancel: Cancelar
|
||||
|
||||
# Intake emails
|
||||
|
||||
intake-emails.title: E-mails de Receção
|
||||
intake-emails.description: Os endereços de e-mail de receção são usados para ingerir automaticamente e-mails no Papra. Basta reencaminhar e-mails para o endereço de e-mail de receção e os seus anexos serão adicionados aos documentos da sua organização.
|
||||
intake-emails.disabled.title: Os E-mails de Receção estão desativados
|
||||
intake-emails.disabled.description: Os e-mails de receção estão desativados nesta instância. Contacte o seu administrador para os ativar. Consulte a {{ documentation }} para mais informações.
|
||||
intake-emails.disabled.documentation: documentação
|
||||
intake-emails.info: Apenas e-mails de receção ativados de origens permitidas serão processados. Pode ativar ou desativar um e-mail de receção a qualquer momento.
|
||||
intake-emails.empty.title: Sem e-mails de receção
|
||||
intake-emails.empty.description: Gere um endereço de receção para ingerir facilmente anexos de e-mails.
|
||||
intake-emails.empty.generate: Gerar e-mail de receção
|
||||
intake-emails.count: '{{ count }} e-mail{{ plural }} de receção para esta organização'
|
||||
intake-emails.new: Novo e-mail de receção
|
||||
intake-emails.disabled-label: (Desativado)
|
||||
intake-emails.no-origins: Sem origens de e-mail permitidas
|
||||
intake-emails.allowed-origins: Permitido de {{ count }} endereço{{ plural }}
|
||||
intake-emails.actions.enable: Ativar
|
||||
intake-emails.actions.disable: Desativar
|
||||
intake-emails.actions.manage-origins: Gerir endereços de origem
|
||||
intake-emails.actions.delete: Eliminar
|
||||
intake-emails.delete.confirm.title: Eliminar e-mail de receção?
|
||||
intake-emails.delete.confirm.message: Tem a certeza de que quer eliminar este e-mail de receção? Esta ação não pode ser desfeita.
|
||||
intake-emails.delete.confirm.confirm-button: Eliminar e-mail de receção
|
||||
intake-emails.delete.confirm.cancel-button: Cancelar
|
||||
intake-emails.delete.success: E-mail de receção eliminado
|
||||
intake-emails.create.success: E-mail de receção criado
|
||||
intake-emails.update.success.enabled: E-mail de receção ativado
|
||||
intake-emails.update.success.disabled: E-mail de receção desativado
|
||||
intake-emails.allowed-origins.title: Origens permitidas
|
||||
intake-emails.allowed-origins.description: Apenas e-mails enviados para {{ email }} destas origens serão processados. Se nenhuma origem for especificada, todos os e-mails serão descartados.
|
||||
intake-emails.allowed-origins.add.label: Adicionar e-mail de origem permitida
|
||||
intake-emails.allowed-origins.add.placeholder: Ex. joao@papra.app
|
||||
intake-emails.allowed-origins.add.button: Adicionar
|
||||
intake-emails.allowed-origins.add.error.exists: Este e-mail já está nas origens permitidas para este e-mail de receção
|
||||
|
||||
# API keys
|
||||
|
||||
api-keys.permissions.documents.title: Documentos
|
||||
api-keys.permissions.documents.documents:create: Criar documentos
|
||||
api-keys.permissions.documents.documents:read: Ler documentos
|
||||
api-keys.permissions.documents.documents:update: Atualizar documentos
|
||||
api-keys.permissions.documents.documents:delete: Eliminar documentos
|
||||
api-keys.permissions.tags.title: Etiquetas
|
||||
api-keys.permissions.tags.tags:create: Criar etiquetas
|
||||
api-keys.permissions.tags.tags:read: Ler etiquetas
|
||||
api-keys.permissions.tags.tags:update: Atualizar etiquetas
|
||||
api-keys.permissions.tags.tags:delete: Eliminar etiquetas
|
||||
api-keys.create.title: Criar chave API
|
||||
api-keys.create.description: Crie uma nova chave API para aceder à API do Papra.
|
||||
api-keys.create.success: A chave API foi criada com sucesso.
|
||||
api-keys.create.back: Voltar às chaves API
|
||||
api-keys.create.form.name.label: Nome
|
||||
api-keys.create.form.name.placeholder: 'Exemplo: A minha chave API'
|
||||
api-keys.create.form.name.required: Por favor, introduza um nome para a chave API
|
||||
api-keys.create.form.permissions.label: Permissões
|
||||
api-keys.create.form.permissions.required: Por favor, selecione pelo menos uma permissão
|
||||
api-keys.create.form.submit: Criar chave API
|
||||
api-keys.create.created.title: Chave API criada
|
||||
api-keys.create.created.description: A chave API foi criada com sucesso. Guarde-a num local seguro pois não será exibida novamente.
|
||||
api-keys.list.title: Chaves API
|
||||
api-keys.list.description: Gira as suas chaves API aqui.
|
||||
api-keys.list.create: Criar chave API
|
||||
api-keys.list.empty.title: Sem chaves API
|
||||
api-keys.list.empty.description: Crie uma chave API para aceder à API do Papra.
|
||||
api-keys.list.card.last-used: Última utilização
|
||||
api-keys.list.card.never: Nunca
|
||||
api-keys.list.card.created: Criado
|
||||
api-keys.delete.success: A chave API foi eliminada com sucesso
|
||||
api-keys.delete.confirm.title: Eliminar chave API
|
||||
api-keys.delete.confirm.message: Tem a certeza de que quer eliminar esta chave API? Esta ação não pode ser desfeita.
|
||||
api-keys.delete.confirm.confirm-button: Eliminar
|
||||
api-keys.delete.confirm.cancel-button: Cancelar
|
||||
|
||||
# Webhooks
|
||||
|
||||
webhooks.list.title: Webhooks
|
||||
webhooks.list.description: Gira os webhooks da sua organização
|
||||
webhooks.list.empty.title: Nenhum webhook
|
||||
webhooks.list.empty.description: Crie o seu primeiro webhook para começar a receber eventos
|
||||
webhooks.list.create: Criar webhook
|
||||
webhooks.list.card.last-triggered: Última ativação
|
||||
webhooks.list.card.never: Nunca
|
||||
webhooks.list.card.created: Criado em
|
||||
webhooks.create.title: Criar webhook
|
||||
webhooks.create.description: Crie um novo webhook para receber eventos
|
||||
webhooks.create.success: Webhook criado com sucesso
|
||||
webhooks.create.back: Voltar
|
||||
webhooks.create.form.submit: Criar webhook
|
||||
webhooks.create.form.name.label: Nome do webhook
|
||||
webhooks.create.form.name.placeholder: Insira o nome do webhook
|
||||
webhooks.create.form.name.required: O nome é obrigatório
|
||||
webhooks.create.form.url.label: URL do Webhook
|
||||
webhooks.create.form.url.placeholder: Insira o URL do webhook
|
||||
webhooks.create.form.url.required: O URL é obrigatória
|
||||
webhooks.create.form.url.invalid: URL inválido
|
||||
webhooks.create.form.secret.label: Segredo
|
||||
webhooks.create.form.secret.placeholder: Insira o segredo do webhook
|
||||
webhooks.create.form.events.label: Eventos
|
||||
webhooks.create.form.events.required: Adicione pelo menos um evento
|
||||
webhooks.update.title: Editar webhook
|
||||
webhooks.update.description: Atualize os detalhes do seu webhook
|
||||
webhooks.update.success: Webhook atualizado com sucesso
|
||||
webhooks.update.submit: Atualizar webhook
|
||||
webhooks.update.cancel: Cancelar
|
||||
webhooks.update.form.secret.placeholder: Insira um novo segredo
|
||||
webhooks.update.form.secret.placeholder-redacted: '[Segredo ocultado]'
|
||||
webhooks.update.form.rotate-secret.button: Rotacionar segredo
|
||||
webhooks.delete.success: Webhook eliminado com sucesso
|
||||
webhooks.delete.confirm.title: Eliminar webhook
|
||||
webhooks.delete.confirm.message: Tem a certeza de que deseja eliminar este webhook?
|
||||
webhooks.delete.confirm.confirm-button: Eliminar
|
||||
webhooks.delete.confirm.cancel-button: Cancelar
|
||||
|
||||
webhooks.events.documents.document:created.description: Documento criado
|
||||
webhooks.events.documents.document:deleted.description: Documento eliminado
|
||||
|
||||
# Navigation
|
||||
|
||||
layout.menu.home: Início
|
||||
layout.menu.documents: Documentos
|
||||
layout.menu.tags: Tags
|
||||
layout.menu.tagging-rules: Regras de etiquetagem
|
||||
layout.menu.deleted-documents: Documentos eliminados
|
||||
layout.menu.organization-settings: Definições
|
||||
layout.menu.api-keys: Chaves API
|
||||
layout.menu.settings: Definições
|
||||
layout.menu.account: Conta
|
||||
layout.menu.general-settings: Definições gerais
|
||||
layout.menu.intake-emails: E-mails de entrada
|
||||
layout.menu.webhooks: Webhooks
|
||||
layout.menu.members: Membros
|
||||
layout.menu.invitations: Convites
|
||||
|
||||
layout.theme.light: Tema claro
|
||||
layout.theme.dark: Tema escuro
|
||||
layout.theme.system: Tema do sistema
|
||||
|
||||
layout.search.placeholder: Procurar...
|
||||
layout.menu.import-document: Importar um documento
|
||||
|
||||
user-menu.account-settings: Definições da conta
|
||||
user-menu.api-keys: Chaves API
|
||||
user-menu.invitations: Convites
|
||||
user-menu.language: Linguagem
|
||||
user-menu.logout: Sair
|
||||
|
||||
# Command palette
|
||||
|
||||
command-palette.search.placeholder: Procurar comandos ou documentos
|
||||
command-palette.no-results: Nenhum resultado encontrado
|
||||
command-palette.sections.documents: Documentos
|
||||
command-palette.sections.theme: Tema
|
||||
|
||||
# API errors
|
||||
|
||||
api-errors.document.already_exists: O documento já existe
|
||||
api-errors.document.file_too_big: O arquivo do documento é muito grande
|
||||
api-errors.intake_email.limit_reached: O número máximo de e-mails de entrada para esta organização foi atingido. Faça um upgrade no seu plano para criar mais e-mails de entrada.
|
||||
api-errors.user.max_organization_count_reached: Atingiu o número máximo de organizações que pode criar. Se precisar de criar mais, entre em contato com o suporte.
|
||||
api-errors.default: Ocorreu um erro ao processar a solicitação.
|
||||
api-errors.organization.invitation_already_exists: Já existe um convite para este e-mail nesta organização.
|
||||
api-errors.user.already_in_organization: Este utilizadpr já faz parte desta organização.
|
||||
api-errors.user.organization_invitation_limit_reached: O número máximo de convites por hoje foi atingido. Por favor, tente novamente amanhã.
|
||||
api-errors.demo.not_available: Este recurso não está disponível em ambiente de demonstração
|
||||
api-errors.tags.already_exists: Já existe uma etiqueta com este nome nesta organização
|
||||
|
||||
# Not found
|
||||
|
||||
not-found.title: 404 - Página não encontrada
|
||||
not-found.description: Desculpe, a página que procura não existe. Verifique o URL e tente novamente.
|
||||
not-found.back-to-home: Voltar para a página inicial
|
||||
|
||||
# Demo
|
||||
|
||||
demo.popup.description: Este é um ambiente de demonstração; todos os dados são guardadis no armazenamento local do navegador.
|
||||
demo.popup.discord: Entre no {{ discordLink }} para obter suporte, sugerir funcionalidades ou apenas conversar.
|
||||
demo.popup.discord-link-label: Comunidade do Discord
|
||||
demo.popup.reset: Redefinir dados da demonstração
|
||||
demo.popup.hide: Ocultar
|
||||
|
||||
# Color picker
|
||||
|
||||
color-picker.hue: Matiz
|
||||
color-picker.saturation: Saturação
|
||||
color-picker.lightness: Brilho
|
||||
color-picker.select-color: Selecionar cor
|
||||
color-picker.select-a-color: Selecione uma cor
|
||||
565
apps/papra-client/src/locales/ro.yml
Normal file
565
apps/papra-client/src/locales/ro.yml
Normal file
@@ -0,0 +1,565 @@
|
||||
# Authentication
|
||||
|
||||
auth.request-password-reset.title: Reseteaza parola
|
||||
auth.request-password-reset.description: Introduceti adresa de email pentru a reseta parola.
|
||||
auth.request-password-reset.requested: Daca exista un cont pentru acest email, am trimis un email cu linkul de resetare.
|
||||
auth.request-password-reset.back-to-login: Inapoi la login
|
||||
auth.request-password-reset.form.email.label: Email
|
||||
auth.request-password-reset.form.email.placeholder: 'Exemplu: popescu@papra.app'
|
||||
auth.request-password-reset.form.email.required: Introduceti adresa de email
|
||||
auth.request-password-reset.form.email.invalid: Adresa email este invalida
|
||||
auth.request-password-reset.form.submit: Trimite cererea de resetare a parolei
|
||||
|
||||
auth.reset-password.title: Reseteaza parola
|
||||
auth.reset-password.description: Introdu o parola noua pentru a o reseta pe cea veche.
|
||||
auth.reset-password.reset: Parola ta a fost resetata cu success.
|
||||
auth.reset-password.back-to-login: Inapoi la login
|
||||
auth.reset-password.form.new-password.label: Parola noua
|
||||
auth.reset-password.form.new-password.placeholder: 'Exemplu: **********'
|
||||
auth.reset-password.form.new-password.required: Introdu noua parola
|
||||
auth.reset-password.form.new-password.min-length: Parola trebuie sa fie de minim {{ minLength }} de caractere
|
||||
auth.reset-password.form.new-password.max-length: Parola trebuie sa fie de maxim {{ maxLength }} de caractere
|
||||
auth.reset-password.form.submit: Reseteaza parola
|
||||
|
||||
auth.email-provider.open: Deschide {{ provider }}
|
||||
|
||||
auth.login.title: Inregistreaza-te pe Papra
|
||||
auth.login.description: Introdu email ul pentru a accesa papra.
|
||||
auth.login.login-with-provider: Inregistreaza-te cu {{ provider }}
|
||||
auth.login.no-account: Nu ai un cont?
|
||||
auth.login.register: Logheaza-te
|
||||
auth.login.form.email.label: email
|
||||
auth.login.form.email.placeholder: 'Exemplu: popescu@papra.app'
|
||||
auth.login.form.email.required: Introduceti adresa de email
|
||||
auth.login.form.email.invalid: Adresa email este invalida
|
||||
auth.login.form.password.label: Parola
|
||||
auth.login.form.password.placeholder: Seteaza o parola noua
|
||||
auth.login.form.password.required: Introduceti parola noua
|
||||
auth.login.form.remember-me.label: Nu ma uita
|
||||
auth.login.form.forgot-password.label: Ai uitat parola?
|
||||
auth.login.form.submit: Logheaza-te
|
||||
|
||||
auth.register.title: Inregistreaza-te pe Papra
|
||||
auth.register.description: Introdu email ul pentru a accesa papra.
|
||||
auth.register.register-with-email: Inregistreaza-te cu email
|
||||
auth.register.register-with-provider: Inregistreaza-te cu {{ provider }}
|
||||
auth.register.providers.google: Google
|
||||
auth.register.providers.github: GitHub
|
||||
auth.register.have-account: Ai deja un cont?
|
||||
auth.register.login: Logheaza-te
|
||||
auth.register.registration-disabled.title: Inregistrarea este dezactivata
|
||||
auth.register.registration-disabled.description: Crearea de conturi noi este momentan dezactivata pe aceasta instanta de Papra. Doar utilizatorii cu conturi existente pot logheaza. Daca crezi ca e o greseala, contacteaza administratorul acestei instante.
|
||||
auth.register.form.email.label: Email
|
||||
auth.register.form.email.placeholder: 'Exemplu: popescu@papra.app'
|
||||
auth.register.form.email.required: Introduceti adresa de email
|
||||
auth.register.form.email.invalid: Adresa email este invalida
|
||||
auth.register.form.password.label: Parola
|
||||
auth.register.form.password.placeholder: Seteaza parola
|
||||
auth.register.form.password.required: Te rugam sa introduci parola
|
||||
auth.register.form.password.min-length: Parola trebuie sa fie de minim {{ minLength }} de caractere
|
||||
auth.register.form.password.max-length: Parola trebuie sa fie de minim {{ maxLength }} de caractere
|
||||
auth.register.form.name.label: Nume
|
||||
auth.register.form.name.placeholder: 'Exemplu: Andrei Popescu'
|
||||
auth.register.form.name.required: Introduce-ti numele
|
||||
auth.register.form.name.max-length: Numele trebuie sa fie de minim {{ maxLength }} de caractere
|
||||
auth.register.form.submit: Inregistreaza-te
|
||||
|
||||
auth.email-validation-required.title: Verifica-ti email-ul
|
||||
auth.email-validation-required.description: A fost trimis un email de verificare la adresa de email introdusa. Verificati email-ul dumneavoastra si click pe link-ul din email.
|
||||
|
||||
auth.legal-links.description: Continuand, confirmati ca intelegeti si sunteti de acord cu {{ terms }} si {{ privacy }}.
|
||||
auth.legal-links.terms: Termenii si conditiile
|
||||
auth.legal-links.privacy: Politica de confidențialitate
|
||||
|
||||
auth.no-auth-provider.title: Niciun provider de autentificare nu este adaugat
|
||||
auth.no-auth-provider.description: Nu exista nicio metoda de autentificare configurata. Contactati administratorul acestei instante pentru a adauga o metoda de autentificare.
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: Setarile tale
|
||||
user.settings.description: Configureaza-ti setarile tale.
|
||||
|
||||
user.settings.email.title: Adresa email
|
||||
user.settings.email.description: Adresa ta de email nu poate fi schimbata.
|
||||
user.settings.email.label: Adresa email
|
||||
|
||||
user.settings.name.title: Numele dvs.
|
||||
user.settings.name.description: Numele dvs. va fi distribuit cu persoanele din organizatia dvs.
|
||||
user.settings.name.label: Numele dvs.
|
||||
user.settings.name.placeholder: Ex. Andrei Popescu
|
||||
user.settings.name.update: Schimba-ti numele
|
||||
user.settings.name.updated: Numele tau s-a schimbat
|
||||
|
||||
user.settings.logout.title: Iesi din cont
|
||||
user.settings.logout.description: Iesi din cont
|
||||
user.settings.logout.button: Iesi din cont
|
||||
|
||||
# Organizations
|
||||
|
||||
organizations.list.title: Organizatiile dvs.
|
||||
organizations.list.description: Organizatiile sunt o modalitate de a grupa documentele si de a le gestiona accesul la acestea. Poti crea multiple organizatii si invita membrii echipei tale sa colabora.
|
||||
organizations.list.create-new: Creeaza o noua organizatie
|
||||
|
||||
organizations.details.no-documents.title: Niciun document
|
||||
organizations.details.no-documents.description: Nu sunt documente in aceasta organizatie. Incepe prin uploadarea unei documente.
|
||||
organizations.details.upload-documents: Incarca documente
|
||||
organizations.details.documents-count: documente in total
|
||||
organizations.details.total-size: marime totala
|
||||
organizations.details.latest-documents: Ultimele documente incarcate
|
||||
|
||||
organizations.create.title: Creeaza o noua organizatie
|
||||
organizations.create.description: Documentele dvs. sunt grupate pe organizatie. Puteti crea mai multe organizatii pentru documente diferite, de exemplu, pentru uz personal si profesional.
|
||||
organizations.create.back: Inapoi
|
||||
organizations.create.error.max-count-reached: Ai ajuns la numarul maxim de organizatii pe care le poti crea, daca ai nevoie de mai multe, contacteaza support ul.
|
||||
organizations.create.form.name.label: Numle organizatiei
|
||||
organizations.create.form.name.placeholder: Ex. Acme SRL.
|
||||
organizations.create.form.name.required: Introdu numele organizatiei
|
||||
organizations.create.form.submit: Creeaza organizatia
|
||||
organizations.create.success: Organizatia a fost creata cu success
|
||||
|
||||
organizations.create-first.title: Creeaza organizatia
|
||||
organizations.create-first.description: Documentele dvs. sunt grupate pe organizatie. Puteti crea mai multe organizatii pentru documente diferite, de exemplu, pentru uz personal si profesional.
|
||||
organizations.create-first.default-name: Organizatia mea
|
||||
organizations.create-first.user-name: 'Organizatia {{ name }}'
|
||||
|
||||
organization.settings.title: Setarile organizatiei
|
||||
organization.settings.page.title: Setarile organizatiei
|
||||
organization.settings.page.description: Gestioneaza setarile organizatiei tale aici.
|
||||
organization.settings.name.title: Numele organizatiei
|
||||
organization.settings.name.update: Actualizeaza numele
|
||||
organization.settings.name.placeholder: Ex. Acme SRL.
|
||||
organization.settings.name.updated: Numele organizatiei a fost actualizat
|
||||
organization.settings.subscription.title: Subscriptie
|
||||
organization.settings.subscription.description: Gestioneaza facturile, facturi si metodele de plata.
|
||||
organization.settings.subscription.manage: Gestioneaza-ti subscriptia
|
||||
organization.settings.subscription.error: Eroare la obtinerea URL-ului portalului client
|
||||
organization.settings.delete.title: Sterge organizatie
|
||||
organization.settings.delete.description: Stergerea acestei organizatii va elimina permanent toate datele asociate cu aceasta.
|
||||
organization.settings.delete.confirm.title: Sterge organizatie
|
||||
organization.settings.delete.confirm.message: Esti sigur ca vrei sa stergi aceasta organizatie? Aceasta operatie nu poate fi anulata si toate datele asociate cu aceasta vor fi eliminate permanent.
|
||||
organization.settings.delete.confirm.confirm-button: Sterge organizatie
|
||||
organization.settings.delete.confirm.cancel-button: Anuleaza
|
||||
organization.settings.delete.success: Organizatie stearsa cu success
|
||||
|
||||
organizations.members.title: Membri
|
||||
organizations.members.description: Gestioneaza membrii organizatiei tale
|
||||
organizations.members.invite-member: Invita membru
|
||||
organizations.members.invite-member-disabled-tooltip: Doar adminii sau proprietarii pot invita membrii la organizatie
|
||||
organizations.members.remove-from-organization: Elimina din organizatie
|
||||
organizations.members.role: Rol
|
||||
organizations.members.roles.owner: Proprietar
|
||||
organizations.members.roles.admin: Admin
|
||||
organizations.members.roles.member: membru
|
||||
organizations.members.delete.confirm.title: Eliminati membrul
|
||||
organizations.members.delete.confirm.message: Esti sigur ca vrei sa stergi acest membru din organizatie?
|
||||
organizations.members.delete.confirm.confirm-button: Elimina
|
||||
organizations.members.delete.confirm.cancel-button: Anuleaza
|
||||
organizations.members.delete.success: membru sters cu succes
|
||||
organizations.members.update-role.success: Rolul membrului a fost actualizat
|
||||
organizations.members.table.headers.name: Nume
|
||||
organizations.members.table.headers.email: Email
|
||||
organizations.members.table.headers.role: Rol
|
||||
organizations.members.table.headers.created: Creat
|
||||
organizations.members.table.headers.actions: Actiuni
|
||||
|
||||
organizations.invite-member.title: Invita membru
|
||||
organizations.invite-member.description: Invita un membru la organizatie
|
||||
organizations.invite-member.form.email.label: Email
|
||||
organizations.invite-member.form.email.placeholder: 'Exemplu: ada@papra.app'
|
||||
organizations.invite-member.form.email.required: Introduceti o adresa de email valida
|
||||
organizations.invite-member.form.role.label: Rol
|
||||
organizations.invite-member.form.submit: Invita membru
|
||||
organizations.invite-member.success.message: membru invitat
|
||||
organizations.invite-member.success.description: Adresa de email a fost invitata la organizatie.
|
||||
organizations.invite-member.error.message: Eroare la invitatia membrului
|
||||
|
||||
organizations.invitations.title: Invitatii
|
||||
organizations.invitations.description: Gestioneaza invitatii la organizatie
|
||||
organizations.invitations.list.cta: Invita membru
|
||||
organizations.invitations.list.empty.title: Niciun invitat
|
||||
organizations.invitations.list.empty.description: Nu ai fost invitat la nicio organizatie inca.
|
||||
organizations.invitations.status.pending: In asteptare
|
||||
organizations.invitations.status.accepted: Acceptat
|
||||
organizations.invitations.status.rejected: Refuzat
|
||||
organizations.invitations.status.expired: Expirat
|
||||
organizations.invitations.status.cancelled: Anulat
|
||||
organizations.invitations.resend: Retrimite invitatia
|
||||
organizations.invitations.cancel.title: Anuleaza invitatia
|
||||
organizations.invitations.cancel.description: Esti sigur ca vrei sa anulezi aceasta invitatie?
|
||||
organizations.invitations.cancel.confirm: Anuleaza invitatia
|
||||
organizations.invitations.cancel.cancel: Anuleaza
|
||||
organizations.invitations.resend.title: Retrimite invitatia
|
||||
organizations.invitations.resend.description: Esti sigur ca vrei sa retrimiteti aceasta invitatie? Acest lucru va trimite un nou email destinatarului.
|
||||
organizations.invitations.resend.confirm: Retrimite invitatia
|
||||
organizations.invitations.resend.cancel: Anuleaza
|
||||
|
||||
invitations.list.title: Invitatii
|
||||
invitations.list.description: Gestioneaza invitatii la organizatie
|
||||
invitations.list.empty.title: Niciun invitat
|
||||
invitations.list.empty.description: Nu ai fost invitat la nicio organizatie inca.
|
||||
invitations.list.headers.organization: Organizatie
|
||||
invitations.list.headers.status: Status
|
||||
invitations.list.headers.created: Creat la
|
||||
invitations.list.headers.actions: Actiuni
|
||||
invitations.list.actions.accept: Accepta
|
||||
invitations.list.actions.reject: Refuza
|
||||
invitations.list.actions.accept.success.message: Invitatie acceptata
|
||||
invitations.list.actions.accept.success.description: Invitatie a fost acceptata.
|
||||
invitations.list.actions.reject.success.message: Invitatie refuzata
|
||||
invitations.list.actions.reject.success.description: Invitatie a fost refuzata.
|
||||
|
||||
# Documents
|
||||
|
||||
documents.list.title: Documente
|
||||
documents.list.no-documents.title: Niciun document
|
||||
documents.list.no-documents.description: Nu exista documente in aceasta organizatie inca. Incepe prin a incarca cateva documente.
|
||||
documents.list.no-results: Niciun document gasit
|
||||
|
||||
documents.tabs.info: Info
|
||||
documents.tabs.content: Continut
|
||||
documents.tabs.activity: Activitate
|
||||
documents.deleted.message: Acest document a fost sters si va fi eliminat permanent in {{ days }} zile.
|
||||
documents.actions.download: Descarca
|
||||
documents.actions.open-in-new-tab: Deschide in fila noua
|
||||
documents.actions.restore: Restaureaza
|
||||
documents.actions.delete: Sterge
|
||||
documents.actions.edit: Editeaza
|
||||
documents.actions.cancel: Anuleaza
|
||||
documents.actions.save: Salveaza
|
||||
documents.actions.saving: Se salveaza...
|
||||
documents.content.alert: Continutul documentului este extras automat din document la incarcare. Este folosit doar pentru cautare si indexare.
|
||||
documents.info.id: ID
|
||||
documents.info.name: Nume
|
||||
documents.info.type: Tip
|
||||
documents.info.size: Dimensiune
|
||||
documents.info.created-at: Creat la
|
||||
documents.info.updated-at: Actualizat la
|
||||
documents.info.never: Niciodata
|
||||
|
||||
documents.rename.title: Redenumeste documentul
|
||||
documents.rename.form.name.label: Nume
|
||||
documents.rename.form.name.placeholder: 'Exemplu: Factura 2024'
|
||||
documents.rename.form.name.required: Va rugam sa introduceti un nume pentru document
|
||||
documents.rename.form.name.max-length: Numele trebuie sa aiba mai putin de 255 de caractere
|
||||
documents.rename.form.submit: Redenumeste documentul
|
||||
documents.rename.success: Document redenumit cu succes
|
||||
documents.rename.cancel: Anuleaza
|
||||
|
||||
import-documents.title.error: '{{ count }} documente au esuat'
|
||||
import-documents.title.success: '{{ count }} documente importate'
|
||||
import-documents.title.pending: '{{ count }} / {{ total }} documente importate'
|
||||
import-documents.title.none: Importa documente
|
||||
import-documents.no-import-in-progress: Niciun import de documente in curs
|
||||
|
||||
documents.deleted.title: Documente sterse
|
||||
documents.deleted.empty.title: Niciun document sters
|
||||
documents.deleted.empty.description: Nu aveti documente sterse. Documentele care sunt sterse vor fi mutate in cosul de gunoi pentru {{ days }} zile.
|
||||
documents.deleted.retention-notice: Toate documentele sterse sunt stocate in cosul de gunoi pentru {{ days }} zile. Dupa acest interval, documentele vor fi sterse permanent si nu le veti putea restaura.
|
||||
documents.deleted.deleted-at: Sterse la
|
||||
documents.deleted.restoring: Se restaureaza...
|
||||
documents.deleted.deleting: Se sterge...
|
||||
|
||||
documents.preview.unknown-file-type: Nicio previzualizare disponibila pentru acest tip de fisier
|
||||
documents.preview.binary-file: Acesta pare a fi un fisier binar si nu poate fi afisat ca text
|
||||
|
||||
trash.delete-all.button: Sterge tot
|
||||
trash.delete-all.confirm.title: Stergeti permanent toate documentele?
|
||||
trash.delete-all.confirm.description: Sunteti sigur ca doriti sa stergeti permanent toate documentele din cosul de gunoi? Aceasta actiune nu poate fi anulata.
|
||||
trash.delete-all.confirm.label: Sterge
|
||||
trash.delete-all.confirm.cancel: Anuleaza
|
||||
trash.delete.button: Sterge
|
||||
trash.delete.confirm.title: Stergeti permanent documentul?
|
||||
trash.delete.confirm.description: Sunteti sigur ca doriti sa stergeti permanent acest document din cosul de gunoi? Aceasta actiune nu poate fi anulata.
|
||||
trash.delete.confirm.label: Sterge
|
||||
trash.delete.confirm.cancel: Anuleaza
|
||||
trash.deleted.success.title: Document sters
|
||||
trash.deleted.success.description: Documentul a fost sters permanent.
|
||||
|
||||
activity.document.created: Documentul a fost creat
|
||||
activity.document.updated.single: Campul {{ field }} a fost actualizat
|
||||
activity.document.updated.multiple: Campurile {{ fields }} au fost actualizate
|
||||
activity.document.updated: Documentul a fost actualizat
|
||||
activity.document.deleted: Documentul a fost sters
|
||||
activity.document.restored: Documentul a fost restaurat
|
||||
activity.document.tagged: Eticheta {{ tag }} a fost adaugata
|
||||
activity.document.untagged: Eticheta {{ tag }} a fost eliminata
|
||||
|
||||
activity.document.user.name: de {{ name }}
|
||||
|
||||
activity.load-more: Incarca mai mult
|
||||
activity.no-more-activities: Nu mai sunt activitati pentru acest document
|
||||
|
||||
# Tags
|
||||
|
||||
tags.no-tags.title: Inca nu exista etichete
|
||||
tags.no-tags.description: Aceasta organizatie nu are inca etichete. Etichetele sunt folosite pentru a clasifica documentele. Puteti adauga etichete la documentele dvs. pentru a le gasi si organiza mai usor.
|
||||
tags.no-tags.create-tag: Creeaza eticheta
|
||||
|
||||
tags.title: Etichete documente
|
||||
tags.description: Etichetele sunt folosite pentru a clasifica documentele. Puteti adauga etichete la documentele dvs. pentru a le gasi si organiza mai usor.
|
||||
tags.create: Creeaza eticheta
|
||||
tags.update: Actualizeaza eticheta
|
||||
tags.delete: Sterge eticheta
|
||||
tags.delete.confirm.title: Sterge eticheta
|
||||
tags.delete.confirm.message: Esti sigur ca vrei sa stergi aceasta eticheta? Stergerea unei etichete o va elimina din toate documentele.
|
||||
tags.delete.confirm.confirm-button: Sterge
|
||||
tags.delete.confirm.cancel-button: Anuleaza
|
||||
tags.delete.success: Eticheta a fost stearsa cu succes
|
||||
tags.create.success: Eticheta "{{ name }}" a fost creata cu succes.
|
||||
tags.update.success: Eticheta "{{ name }}" a fost actualizata cu succes.
|
||||
tags.form.name.label: Nume
|
||||
tags.form.name.placeholder: Ex. Contracte
|
||||
tags.form.name.required: Va rugam sa introduceti un nume de eticheta
|
||||
tags.form.name.max-length: Numele etichetei trebuie sa aiba mai putin de 64 de caractere
|
||||
tags.form.color.label: Culoare
|
||||
tags.form.color.required: Va rugam sa introduceti o culoare
|
||||
tags.form.color.invalid: Culoarea hex este formatata gresit.
|
||||
tags.form.description.label: Descriere
|
||||
tags.form.description.optional: (optional)
|
||||
tags.form.description.placeholder: Ex. Toate contractele semnate de companie
|
||||
tags.form.description.max-length: Descrierea trebuie sa aiba mai putin de 256 de caractere
|
||||
tags.form.no-description: Nicio descriere
|
||||
tags.table.headers.tag: Eticheta
|
||||
tags.table.headers.description: Descriere
|
||||
tags.table.headers.documents: Documente
|
||||
tags.table.headers.created: Creat la
|
||||
tags.table.headers.actions: Actiuni
|
||||
|
||||
# Tagging rules
|
||||
|
||||
tagging-rules.field.name: nume document
|
||||
tagging-rules.field.content: continut document
|
||||
tagging-rules.operator.equals: este egal cu
|
||||
tagging-rules.operator.not-equals: nu este egal cu
|
||||
tagging-rules.operator.contains: contine
|
||||
tagging-rules.operator.not-contains: nu contine
|
||||
tagging-rules.operator.starts-with: incepe cu
|
||||
tagging-rules.operator.ends-with: se termina cu
|
||||
tagging-rules.list.title: Reguli de etichetare
|
||||
tagging-rules.list.description: Gestioneaza regulile de etichetare ale organizatiei tale, pentru a eticheta automat documentele pe baza conditiilor pe care le definesti.
|
||||
tagging-rules.list.demo-warning: 'Nota: Deoarece acesta este un mediu demonstrativ (fara server), regulile de etichetare nu vor fi aplicate documentelor nou adaugate.'
|
||||
tagging-rules.list.no-tagging-rules.title: Nicio regula de etichetare
|
||||
tagging-rules.list.no-tagging-rules.description: Creati o regula de etichetare pentru a eticheta automat documentele adaugate pe baza conditiilor pe care le definiti.
|
||||
tagging-rules.list.no-tagging-rules.create-tagging-rule: Creeaza regula de etichetare
|
||||
tagging-rules.list.card.no-conditions: Nicio conditie
|
||||
tagging-rules.list.card.one-condition: 1 conditie
|
||||
tagging-rules.list.card.conditions: '{{ count }} conditii'
|
||||
tagging-rules.list.card.delete: Sterge regula
|
||||
tagging-rules.list.card.edit: Editeaza regula
|
||||
tagging-rules.create.title: Creeaza regula de etichetare
|
||||
tagging-rules.create.success: Regula de etichetare a fost creata cu succes
|
||||
tagging-rules.create.error: Nu s-a putut crea regula de etichetare
|
||||
tagging-rules.create.submit: Creeaza regula
|
||||
tagging-rules.form.name.label: Nume
|
||||
tagging-rules.form.name.placeholder: 'Exemplu: Eticheteaza facturile'
|
||||
tagging-rules.form.name.min-length: Va rugam sa introduceti un nume pentru regula
|
||||
tagging-rules.form.name.max-length: Numele trebuie sa aiba mai putin de 64 de caractere
|
||||
tagging-rules.form.description.label: Descriere
|
||||
tagging-rules.form.description.placeholder: "Exemplu: Eticheteaza documentele cu 'factura' in nume"
|
||||
tagging-rules.form.description.max-length: Descrierea trebuie sa aiba mai putin de 256 de caractere
|
||||
tagging-rules.form.conditions.label: Conditii
|
||||
tagging-rules.form.conditions.description: Definiti conditiile care trebuie indeplinite pentru ca regula sa se aplice. Toate conditiile trebuie indeplinite pentru ca regula sa se aplice.
|
||||
tagging-rules.form.conditions.add-condition: Adauga conditie
|
||||
tagging-rules.form.conditions.no-conditions.title: Nicio conditie
|
||||
tagging-rules.form.conditions.no-conditions.description: Nu ati adaugat nicio conditie acestei reguli. Aceasta regula va aplica etichetele sale tuturor documentelor.
|
||||
tagging-rules.form.conditions.no-conditions.confirm: Aplica regula fara conditii
|
||||
tagging-rules.form.conditions.no-conditions.cancel: Anuleaza
|
||||
tagging-rules.form.conditions.value.placeholder: 'Exemplu: factura'
|
||||
tagging-rules.form.conditions.value.min-length: Va rugam sa introduceti o valoare pentru conditie
|
||||
tagging-rules.form.tags.label: Etichete
|
||||
tagging-rules.form.tags.description: Selecteaza etichetele de aplicat documentelor adaugate care corespund conditiilor
|
||||
tagging-rules.form.tags.min-length: Este necesara cel putin o eticheta de aplicat
|
||||
tagging-rules.form.tags.add-tag: Creeaza eticheta
|
||||
tagging-rules.form.submit: Creeaza regula
|
||||
tagging-rules.update.title: Actualizeaza regula de etichetare
|
||||
tagging-rules.update.error: Nu s-a putut actualiza regula de etichetare
|
||||
tagging-rules.update.submit: Actualizeaza regula
|
||||
tagging-rules.update.cancel: Anuleaza
|
||||
|
||||
# Intake emails
|
||||
|
||||
intake-emails.title: Email-uri de preluare
|
||||
intake-emails.description: Adresele de email de preluare sunt folosite pentru a introduce automat email-uri in Papra. Doar trimiteti email-uri catre adresa de email de preluare, iar atasamentele lor vor fi adaugate la documentele organizatiei dvs.
|
||||
intake-emails.disabled.title: Email-urile de preluare sunt dezactivate
|
||||
intake-emails.disabled.description: Email-urile de preluare sunt dezactivate pe aceasta instanta. Va rugam sa contactati administratorul pentru a le activa. Consultati {{ documentation }} pentru mai multe informatii.
|
||||
intake-emails.disabled.documentation: documentatie
|
||||
intake-emails.info: Doar email-urile de preluare activate de la origini permise vor fi procesate. Puteti activa sau dezactiva un email de preluare oricand.
|
||||
intake-emails.empty.title: Niciun email de preluare
|
||||
intake-emails.empty.description: Generati o adresa de preluare pentru a ingera cu usurinta atasamentele de email.
|
||||
intake-emails.empty.generate: Genereaza email de preluare
|
||||
intake-emails.count: '{{ count }} email{{ plural }} de preluare pentru aceasta organizatie'
|
||||
intake-emails.new: Email nou de preluare
|
||||
intake-emails.disabled-label: (Dezactivat)
|
||||
intake-emails.no-origins: Nicio origine de email permisa
|
||||
intake-emails.allowed-origins: Permis de la {{ count }} adrese{{ plural }}
|
||||
intake-emails.actions.enable: Activeaza
|
||||
intake-emails.actions.disable: Dezactiveaza
|
||||
intake-emails.actions.manage-origins: Gestioneaza adresele de origine
|
||||
intake-emails.actions.delete: Sterge
|
||||
intake-emails.delete.confirm.title: Sterge email-ul de preluare?
|
||||
intake-emails.delete.confirm.message: Esti sigur ca vrei sa stergi acest email de preluare? Aceasta actiune nu poate fi anulata.
|
||||
intake-emails.delete.confirm.confirm-button: Sterge email-ul de preluare
|
||||
intake-emails.delete.confirm.cancel-button: Anuleaza
|
||||
intake-emails.delete.success: Email de preluare sters
|
||||
intake-emails.create.success: Email de preluare creat
|
||||
intake-emails.update.success.enabled: Email de preluare activat
|
||||
intake-emails.update.success.disabled: Email de preluare dezactivat
|
||||
intake-emails.allowed-origins.title: Origini permise
|
||||
intake-emails.allowed-origins.description: Doar email-urile trimise la {{ email }} de la aceste origini vor fi procesate. Daca nu sunt specificate origini, toate email-urile vor fi ignorate.
|
||||
intake-emails.allowed-origins.add.label: Adauga adresa de email de origine permisa
|
||||
intake-emails.allowed-origins.add.placeholder: Ex. ada@papra.app
|
||||
intake-emails.allowed-origins.add.button: Adauga
|
||||
intake-emails.allowed-origins.add.error.exists: Acest email este deja in originile permise pentru acest email de preluare
|
||||
|
||||
# API keys
|
||||
|
||||
api-keys.permissions.documents.title: Documente
|
||||
api-keys.permissions.documents.documents:create: Creaza documente
|
||||
api-keys.permissions.documents.documents:read: Citeste documente
|
||||
api-keys.permissions.documents.documents:update: Actualizeaza documente
|
||||
api-keys.permissions.documents.documents:delete: Sterge documente
|
||||
api-keys.permissions.tags.title: Etichete
|
||||
api-keys.permissions.tags.tags:create: Creaza etichete
|
||||
api-keys.permissions.tags.tags:read: Citeste etichete
|
||||
api-keys.permissions.tags.tags:update: Actualizeaza etichete
|
||||
api-keys.permissions.tags.tags:delete: Sterge etichete
|
||||
api-keys.create.title: Creeaza cheie API
|
||||
api-keys.create.description: Creeaza o noua cheie API pentru a accesa API-ul Papra.
|
||||
api-keys.create.success: Cheia API a fost creata cu succes.
|
||||
api-keys.create.back: Inapoi la cheile API
|
||||
api-keys.create.form.name.label: Nume
|
||||
api-keys.create.form.name.placeholder: 'Exemplu: Cheia mea API'
|
||||
api-keys.create.form.name.required: Va rugam sa introduceti un nume pentru cheia API
|
||||
api-keys.create.form.permissions.label: Permisiuni
|
||||
api-keys.create.form.permissions.required: Va rugam sa selectati cel putin o permisiune
|
||||
api-keys.create.form.submit: Creeaza cheie API
|
||||
api-keys.create.created.title: Cheie API creata
|
||||
api-keys.create.created.description: Cheia API a fost creata cu succes. Salvati-o intr-o locatie sigura, deoarece nu va mai fi afisata.
|
||||
api-keys.list.title: Chei API
|
||||
api-keys.list.description: Gestioneaza-ti cheile API aici.
|
||||
api-keys.list.create: Creeaza cheie API
|
||||
api-keys.list.empty.title: Nicio cheie API
|
||||
api-keys.list.empty.description: Creeaza o cheie API pentru a accesa API-ul Papra.
|
||||
api-keys.list.card.last-used: Ultima utilizare
|
||||
api-keys.list.card.never: Niciodata
|
||||
api-keys.list.card.created: Creat la
|
||||
api-keys.delete.success: Cheia API a fost stearsa cu succes
|
||||
api-keys.delete.confirm.title: Sterge cheia API
|
||||
api-keys.delete.confirm.message: Esti sigur ca vrei sa stergi aceasta cheie API? Aceasta actiune nu poate fi anulata.
|
||||
api-keys.delete.confirm.confirm-button: Sterge
|
||||
api-keys.delete.confirm.cancel-button: Anuleaza
|
||||
|
||||
# Webhooks
|
||||
|
||||
webhooks.list.title: Webhook-uri
|
||||
webhooks.list.description: Gestioneaza webhook-urile organizatiei tale
|
||||
webhooks.list.empty.title: Niciun webhook
|
||||
webhooks.list.empty.description: Creeaza primul tau webhook pentru a incepe sa primesti evenimente
|
||||
webhooks.list.create: Creeaza webhook
|
||||
webhooks.list.card.last-triggered: Ultima declansare
|
||||
webhooks.list.card.never: Niciodata
|
||||
webhooks.list.card.created: Creat la
|
||||
webhooks.create.title: Creeaza webhook
|
||||
webhooks.create.description: Creeaza un nou webhook pentru a primi evenimente
|
||||
webhooks.create.success: Webhook creat cu succes
|
||||
webhooks.create.back: Inapoi
|
||||
webhooks.create.form.submit: Creeaza webhook
|
||||
webhooks.create.form.name.label: Nume webhook
|
||||
webhooks.create.form.name.placeholder: Introdu numele webhook-ului
|
||||
webhooks.create.form.name.required: Numele este obligatoriu
|
||||
webhooks.create.form.url.label: URL webhook
|
||||
webhooks.create.form.url.placeholder: Introdu URL-ul webhook-ului
|
||||
webhooks.create.form.url.required: URL-ul este obligatoriu
|
||||
webhooks.create.form.url.invalid: URL-ul este invalid
|
||||
webhooks.create.form.secret.label: Secret
|
||||
webhooks.create.form.secret.placeholder: Introdu secretul webhook-ului
|
||||
webhooks.create.form.events.label: Evenimente
|
||||
webhooks.create.form.events.required: Este necesar cel putin un eveniment
|
||||
webhooks.update.title: Editeaza webhook
|
||||
webhooks.update.description: Actualizeaza detaliile webhook-ului tau
|
||||
webhooks.update.success: Webhook actualizat cu succes
|
||||
webhooks.update.submit: Actualizeaza webhook
|
||||
webhooks.update.cancel: Anuleaza
|
||||
webhooks.update.form.secret.placeholder: Introdu un nou secret
|
||||
webhooks.update.form.secret.placeholder-redacted: '[Secret redactat]'
|
||||
webhooks.update.form.rotate-secret.button: Roteste secretul
|
||||
webhooks.delete.success: Webhook sters cu succes
|
||||
webhooks.delete.confirm.title: Sterge webhook
|
||||
webhooks.delete.confirm.message: Esti sigur ca vrei sa stergi acest webhook?
|
||||
webhooks.delete.confirm.confirm-button: Sterge
|
||||
webhooks.delete.confirm.cancel-button: Anuleaza
|
||||
|
||||
webhooks.events.documents.document:created.description: Document creat
|
||||
webhooks.events.documents.document:deleted.description: Document sters
|
||||
|
||||
# Navigation
|
||||
|
||||
layout.menu.home: Acasa
|
||||
layout.menu.documents: Documente
|
||||
layout.menu.tags: Etichete
|
||||
layout.menu.tagging-rules: Reguli de etichetare
|
||||
layout.menu.deleted-documents: Documente sterse
|
||||
layout.menu.organization-settings: Setari organizatie
|
||||
layout.menu.api-keys: Chei API
|
||||
layout.menu.settings: Setari
|
||||
layout.menu.account: Cont
|
||||
layout.menu.general-settings: Setari generale
|
||||
layout.menu.intake-emails: Email-uri de preluare
|
||||
layout.menu.webhooks: Webhook-uri
|
||||
layout.menu.members: Membri
|
||||
layout.menu.invitations: Invitatii
|
||||
|
||||
layout.theme.light: Mod luminos
|
||||
layout.theme.dark: Mod intunecat
|
||||
layout.theme.system: Mod sistem
|
||||
|
||||
layout.search.placeholder: Cauta...
|
||||
layout.menu.import-document: Importa un document
|
||||
|
||||
user-menu.account-settings: Setari cont
|
||||
user-menu.api-keys: Chei API
|
||||
user-menu.invitations: Invitatii
|
||||
user-menu.language: Limba
|
||||
user-menu.logout: Deconectare
|
||||
|
||||
# Command palette
|
||||
|
||||
command-palette.search.placeholder: Cauta comenzi sau documente
|
||||
command-palette.no-results: Niciun rezultat gasit
|
||||
command-palette.sections.documents: Documente
|
||||
command-palette.sections.theme: Tema
|
||||
|
||||
# API errors
|
||||
|
||||
api-errors.document.already_exists: Documentul exista deja
|
||||
api-errors.document.file_too_big: Fisierul documentului este prea mare
|
||||
api-errors.intake_email.limit_reached: Numarul maxim de email-uri de preluare pentru aceasta organizatie a fost atins. Va rugam sa va imbunatatiti planul pentru a crea mai multe email-uri de preluare.
|
||||
api-errors.user.max_organization_count_reached: Ai atins numarul maxim de organizatii pe care le poti crea, daca ai nevoie sa creezi mai multe, te rugam sa contactezi suportul.
|
||||
api-errors.default: A aparut o eroare la procesarea cererii tale.
|
||||
api-errors.organization.invitation_already_exists: O invitatie pentru acest email exista deja in aceasta organizatie.
|
||||
api-errors.user.already_in_organization: Acest utilizator este deja in aceasta organizatie.
|
||||
api-errors.user.organization_invitation_limit_reached: Numarul maxim de invitatii a fost atins pentru astazi. Va rugam sa incercati din nou maine.
|
||||
api-errors.demo.not_available: Aceasta functie nu este disponibila in demo
|
||||
api-errors.tags.already_exists: O eticheta cu acest nume exista deja pentru aceasta organizatie
|
||||
|
||||
# Not found
|
||||
|
||||
not-found.title: 404 - Nu a fost gasit
|
||||
not-found.description: Ne pare rau, pagina pe care o cautati nu pare sa existe. Va rugam sa verificati URL-ul si sa incercati din nou.
|
||||
not-found.back-to-home: Inapoi la pagina principala
|
||||
|
||||
# Demo
|
||||
|
||||
demo.popup.description: Acesta este un mediu demonstrativ, toate datele sunt salvate in stocarea locala a browserului dumneavoastra.
|
||||
demo.popup.discord: Alaturati-va {{ discordLink }} pentru a obtine suport, a propune functionalitati sau doar pentru a discuta.
|
||||
demo.popup.discord-link-label: server Discord
|
||||
demo.popup.reset: Reseteaza datele demo
|
||||
demo.popup.hide: Ascunde
|
||||
|
||||
# Color picker
|
||||
|
||||
color-picker.hue: Nuanta
|
||||
color-picker.saturation: Saturatie
|
||||
color-picker.lightness: Luminozitate
|
||||
color-picker.select-color: Selecteaza culoarea
|
||||
color-picker.select-a-color: Selecteaza o culoare
|
||||
28
apps/papra-client/src/modules/api-keys/api-keys.constants.ts
Normal file
28
apps/papra-client/src/modules/api-keys/api-keys.constants.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// export const API_KEY_PERMISSIONS = {
|
||||
// documents: {
|
||||
// create: 'documents:create',
|
||||
// },
|
||||
// } as const;
|
||||
|
||||
export const API_KEY_PERMISSIONS = [
|
||||
{
|
||||
section: 'documents',
|
||||
permissions: [
|
||||
'documents:create',
|
||||
'documents:read',
|
||||
'documents:update',
|
||||
'documents:delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'tags',
|
||||
permissions: [
|
||||
'tags:create',
|
||||
'tags:read',
|
||||
'tags:update',
|
||||
'tags:delete',
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const API_KEY_PERMISSIONS_LIST = API_KEY_PERMISSIONS.flatMap(permission => permission.permissions);
|
||||
56
apps/papra-client/src/modules/api-keys/api-keys.services.ts
Normal file
56
apps/papra-client/src/modules/api-keys/api-keys.services.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { ApiKey } from './api-keys.types';
|
||||
import { apiClient } from '../shared/http/api-client';
|
||||
import { coerceDates } from '../shared/http/http-client.models';
|
||||
|
||||
export async function createApiKey({
|
||||
name,
|
||||
permissions,
|
||||
organizationIds,
|
||||
allOrganizations,
|
||||
expiresAt,
|
||||
}: {
|
||||
name: string;
|
||||
permissions: string[];
|
||||
organizationIds: string[];
|
||||
allOrganizations: boolean;
|
||||
expiresAt?: Date;
|
||||
}) {
|
||||
const { apiKey, token } = await apiClient<{
|
||||
apiKey: ApiKey;
|
||||
token: string;
|
||||
}>({
|
||||
path: '/api/api-keys',
|
||||
method: 'POST',
|
||||
body: {
|
||||
name,
|
||||
permissions,
|
||||
organizationIds,
|
||||
allOrganizations,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
apiKey: coerceDates(apiKey),
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchApiKeys() {
|
||||
const { apiKeys } = await apiClient<{
|
||||
apiKeys: ApiKey[];
|
||||
}>({
|
||||
path: '/api/api-keys',
|
||||
});
|
||||
|
||||
return {
|
||||
apiKeys: apiKeys.map(coerceDates),
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteApiKey({ apiKeyId }: { apiKeyId: string }) {
|
||||
await apiClient({
|
||||
path: `/api/api-keys/${apiKeyId}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
12
apps/papra-client/src/modules/api-keys/api-keys.types.ts
Normal file
12
apps/papra-client/src/modules/api-keys/api-keys.types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
organizationIds: string[];
|
||||
allOrganizations: boolean;
|
||||
expiresAt?: Date;
|
||||
prefix: string;
|
||||
lastUsedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import type { LocaleKeys } from '@/modules/i18n/locales.types';
|
||||
import { createSignal, For } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
||||
import { API_KEY_PERMISSIONS } from '../api-keys.constants';
|
||||
|
||||
export const ApiKeyPermissionsPicker: Component<{ permissions: string[]; onChange: (permissions: string[]) => void }> = (props) => {
|
||||
const [permissions, setPermissions] = createSignal<string[]>(props.permissions);
|
||||
const { t } = useI18n();
|
||||
|
||||
const getPermissionsSections = () => {
|
||||
return API_KEY_PERMISSIONS.map(section => ({
|
||||
...section,
|
||||
title: t(`api-keys.permissions.${section.section}.title`),
|
||||
permissions: section.permissions.map((permission) => {
|
||||
const [prefix, suffix] = permission.split(':');
|
||||
|
||||
return {
|
||||
name: permission,
|
||||
prefix,
|
||||
suffix,
|
||||
description: t(`api-keys.permissions.${section.section}.${permission}` as LocaleKeys),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
const isPermissionSelected = (permission: string) => {
|
||||
return permissions().includes(permission);
|
||||
};
|
||||
|
||||
const togglePermission = (permission: string) => {
|
||||
setPermissions((prev) => {
|
||||
if (prev.includes(permission)) {
|
||||
return prev.filter(p => p !== permission);
|
||||
}
|
||||
|
||||
return [...prev, permission];
|
||||
});
|
||||
|
||||
props.onChange(permissions());
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<For each={getPermissionsSections()}>
|
||||
{section => (
|
||||
<div>
|
||||
<p class="text-muted-foreground text-xs">{section.title}</p>
|
||||
|
||||
<div class="pl-4 flex flex-col gap-4 mt-4">
|
||||
<For each={section.permissions}>
|
||||
{permission => (
|
||||
<Checkbox
|
||||
class="flex items-center gap-2"
|
||||
checked={isPermissionSelected(permission.name)}
|
||||
onChange={() => togglePermission(permission.name)}
|
||||
>
|
||||
<CheckboxControl />
|
||||
<div class="flex flex-col gap-1">
|
||||
<CheckboxLabel class="text-sm leading-none">
|
||||
{permission.description}
|
||||
</CheckboxLabel>
|
||||
</div>
|
||||
</Checkbox>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
140
apps/papra-client/src/modules/api-keys/pages/api-keys.page.tsx
Normal file
140
apps/papra-client/src/modules/api-keys/pages/api-keys.page.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import type { ApiKey } from '../api-keys.types';
|
||||
import { A } from '@solidjs/router';
|
||||
import { useMutation, useQuery } from '@tanstack/solid-query';
|
||||
import { format } from 'date-fns';
|
||||
import { For, Match, Show, Suspense, Switch } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||
import { queryClient } from '@/modules/shared/query/query-client';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { EmptyState } from '@/modules/ui/components/empty';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
import { deleteApiKey, fetchApiKeys } from '../api-keys.services';
|
||||
|
||||
export const ApiKeyCard: Component<{ apiKey: ApiKey }> = ({ apiKey }) => {
|
||||
const { t } = useI18n();
|
||||
const { confirm } = useConfirmModal();
|
||||
|
||||
const deleteApiKeyMutation = useMutation(() => ({
|
||||
mutationFn: deleteApiKey,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['api-keys'] });
|
||||
createToast({
|
||||
message: t('api-keys.delete.success'),
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirmed = await confirm({
|
||||
title: t('api-keys.delete.confirm.title'),
|
||||
message: t('api-keys.delete.confirm.message'),
|
||||
confirmButton: {
|
||||
text: t('api-keys.delete.confirm.confirm-button'),
|
||||
variant: 'destructive',
|
||||
},
|
||||
cancelButton: {
|
||||
text: t('api-keys.delete.confirm.cancel-button'),
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteApiKeyMutation.mutate({ apiKeyId: apiKey.id });
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="bg-card rounded-lg border p-4 flex items-center gap-4">
|
||||
<div class="rounded-lg bg-muted p-2">
|
||||
<div class="i-tabler-key text-muted-foreground size-5 text-primary" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-sm font-medium leading-tight">{apiKey.name}</h2>
|
||||
<p class="text-muted-foreground text-xs font-mono">{`${apiKey.prefix}...`}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{t('api-keys.list.card.last-used')}
|
||||
{' '}
|
||||
{apiKey.lastUsedAt ? format(apiKey.lastUsedAt, 'MMM d, yyyy') : t('api-keys.list.card.never')}
|
||||
</p>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{t('api-keys.list.card.created')}
|
||||
{' '}
|
||||
{format(apiKey.createdAt, 'MMM d, yyyy')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
isLoading={deleteApiKeyMutation.isPending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<div class="i-tabler-trash text-muted-foreground size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ApiKeysPage: Component = () => {
|
||||
const { t } = useI18n();
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['api-keys'],
|
||||
queryFn: () => fetchApiKeys(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div class="p-6 mt-12 pb-32 mx-auto max-w-xl w-full">
|
||||
<div class="border-b pb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold mb-1">{t('api-keys.list.title')}</h1>
|
||||
<p class="text-muted-foreground">{t('api-keys.list.description')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Show when={query.data?.apiKeys?.length}>
|
||||
<Button as={A} href="/api-keys/create" class="gap-2">
|
||||
<div class="i-tabler-plus size-4" />
|
||||
{t('api-keys.list.create')}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense>
|
||||
<Switch>
|
||||
<Match when={query.data?.apiKeys?.length === 0}>
|
||||
<EmptyState
|
||||
title={t('api-keys.list.empty.title')}
|
||||
description={t('api-keys.list.empty.description')}
|
||||
icon="i-tabler-key"
|
||||
cta={(
|
||||
<Button as={A} href="/api-keys/create" class="gap-2">
|
||||
<div class="i-tabler-plus size-4" />
|
||||
{t('api-keys.list.create')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</Match>
|
||||
|
||||
<Match when={query.data?.apiKeys?.length}>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-2">
|
||||
<For each={query.data?.apiKeys}>
|
||||
{apiKey => (
|
||||
<ApiKeyCard apiKey={apiKey} />
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { setValue } from '@modular-forms/solid';
|
||||
import { A } from '@solidjs/router';
|
||||
import { createSignal, Show } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { createForm } from '@/modules/shared/form/form';
|
||||
import { queryClient } from '@/modules/shared/query/query-client';
|
||||
import { CopyButton } from '@/modules/shared/utils/copy';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { API_KEY_PERMISSIONS_LIST } from '../api-keys.constants';
|
||||
import { createApiKey } from '../api-keys.services';
|
||||
import { ApiKeyPermissionsPicker } from '../components/api-key-permissions-picker.component';
|
||||
|
||||
export const CreateApiKeyPage: Component = () => {
|
||||
const { t } = useI18n();
|
||||
const [getToken, setToken] = createSignal<string | null>(null);
|
||||
|
||||
const { form, Form, Field } = createForm({
|
||||
onSubmit: async ({ name, permissions }) => {
|
||||
const { token } = await createApiKey({
|
||||
name,
|
||||
permissions,
|
||||
organizationIds: [],
|
||||
allOrganizations: false,
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ['api-keys'] });
|
||||
|
||||
setToken(token);
|
||||
|
||||
createToast({
|
||||
type: 'success',
|
||||
message: t('api-keys.create.success'),
|
||||
});
|
||||
},
|
||||
schema: v.object({
|
||||
name: v.pipe(
|
||||
v.string(),
|
||||
v.nonEmpty(t('api-keys.create.form.name.required')),
|
||||
),
|
||||
permissions: v.pipe(
|
||||
v.array(v.picklist(API_KEY_PERMISSIONS_LIST as string[])),
|
||||
v.nonEmpty(t('api-keys.create.form.permissions.required')),
|
||||
),
|
||||
}),
|
||||
initialValues: {
|
||||
name: '',
|
||||
permissions: API_KEY_PERMISSIONS_LIST,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="p-6 mt-12 pb-32 mx-auto max-w-xl w-full">
|
||||
<div class="border-b pb-4 mb-6">
|
||||
<h1 class="text-2xl font-bold">{t('api-keys.create.title')}</h1>
|
||||
<p class="text-sm text-muted-foreground">{t('api-keys.create.description')}</p>
|
||||
</div>
|
||||
|
||||
<Show when={getToken()}>
|
||||
<div class="bg-card border p-6 rounded-md mt-6">
|
||||
<h2 class="text-lg font-semibold mb-2">{t('api-keys.create.created.title')}</h2>
|
||||
<p class="text-sm text-muted-foreground mb-4">{t('api-keys.create.created.description')}</p>
|
||||
|
||||
<TextFieldRoot class="flex items-center gap-2 space-y-0">
|
||||
<TextField type="text" placeholder={t('api-keys.create.form.name.placeholder')} value={getToken() ?? ''} />
|
||||
<CopyButton text={getToken() ?? ''} />
|
||||
</TextFieldRoot>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-6">
|
||||
<Button type="button" variant="secondary" as={A} href="/api-keys">
|
||||
{t('api-keys.create.back')}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!getToken()}>
|
||||
<Form>
|
||||
<Field name="name">
|
||||
{(field, inputProps) => (
|
||||
|
||||
<TextFieldRoot class="flex flex-col mb-6">
|
||||
<TextFieldLabel for="name">{t('api-keys.create.form.name.label')}</TextFieldLabel>
|
||||
<TextField type="text" id="name" placeholder={t('api-keys.create.form.name.placeholder')} {...inputProps} autoFocus value={field.value} aria-invalid={Boolean(field.error)} />
|
||||
{field.error && <div class="text-red-500 text-sm">{field.error}</div>}
|
||||
</TextFieldRoot>
|
||||
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name="permissions" type="string[]">
|
||||
{field => (
|
||||
<div>
|
||||
<p class="text-sm font-bold">{t('api-keys.create.form.permissions.label')}</p>
|
||||
|
||||
<div class="p-6 pb-8 border rounded-md mt-2">
|
||||
<ApiKeyPermissionsPicker permissions={field.value ?? []} onChange={permissions => setValue(form, 'permissions', permissions)} />
|
||||
</div>
|
||||
|
||||
{field.error && <div class="text-red-500 text-sm">{field.error}</div>}
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div class="flex justify-end mt-6">
|
||||
<Button type="submit" isLoading={form.submitting}>
|
||||
{t('api-keys.create.form.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Config } from '../config/config';
|
||||
import type { SsoProviderConfig } from './auth.types';
|
||||
import { get } from 'lodash-es';
|
||||
import { ssoProviders } from './auth.constants';
|
||||
|
||||
@@ -8,8 +9,15 @@ export function isAuthErrorWithCode({ error, code }: { error: unknown; code: str
|
||||
|
||||
export const isEmailVerificationRequiredError = ({ error }: { error: unknown }) => isAuthErrorWithCode({ error, code: 'EMAIL_NOT_VERIFIED' });
|
||||
|
||||
export function getEnabledSsoProviderConfigs({ config }: { config: Config }) {
|
||||
const enabledSsoProviders = ssoProviders.filter(({ key }) => get(config, `auth.providers.${key}.isEnabled`));
|
||||
export function getEnabledSsoProviderConfigs({ config }: { config: Config }): SsoProviderConfig[] {
|
||||
const enabledSsoProviders: SsoProviderConfig[] = [
|
||||
...ssoProviders.filter(({ key }) => get(config, `auth.providers.${key}.isEnabled`)),
|
||||
...config.auth.providers.customs.map(({ providerId, providerName, providerIconUrl }) => ({
|
||||
key: providerId,
|
||||
name: providerName,
|
||||
icon: providerIconUrl ?? 'i-tabler-login-2',
|
||||
})),
|
||||
];
|
||||
|
||||
return enabledSsoProviders;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { createAuthClient as createBetterAuthClient } from 'better-auth/solid';
|
||||
import type { Config } from '../config/config';
|
||||
|
||||
import type { SsoProviderConfig } from './auth.types';
|
||||
import { genericOAuthClient } from 'better-auth/client/plugins';
|
||||
import { createAuthClient as createBetterAuthClient } from 'better-auth/solid';
|
||||
import { buildTimeConfig } from '../config/config';
|
||||
import { trackingServices } from '../tracking/tracking.services';
|
||||
import { createDemoAuthClient } from './auth.demo.services';
|
||||
@@ -7,6 +10,9 @@ import { createDemoAuthClient } from './auth.demo.services';
|
||||
export function createAuthClient() {
|
||||
const client = createBetterAuthClient({
|
||||
baseURL: buildTimeConfig.baseApiUrl,
|
||||
plugins: [
|
||||
genericOAuthClient(),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -38,3 +44,17 @@ export const {
|
||||
} = buildTimeConfig.isDemoMode
|
||||
? createDemoAuthClient()
|
||||
: createAuthClient();
|
||||
|
||||
export async function authWithProvider({ provider, config }: { provider: SsoProviderConfig; config: Config }) {
|
||||
const isCustomProvider = config.auth.providers.customs.some(({ providerId }) => providerId === provider.key);
|
||||
|
||||
if (isCustomProvider) {
|
||||
signIn.oauth2({
|
||||
providerId: provider.key,
|
||||
callbackURL: config.baseUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await signIn.social({ provider: provider.key as 'github' | 'google', callbackURL: config.baseUrl });
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ssoProviders } from './auth.constants';
|
||||
|
||||
export type SsoProviderKey = (typeof ssoProviders)[number]['key'];
|
||||
export type SsoProviderKey = (typeof ssoProviders)[number]['key'] | string & {};
|
||||
export type SsoProviderConfig = { key: SsoProviderKey; name: string; icon: string };
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { A } from '@solidjs/router';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { createVitrineUrl } from '@/modules/shared/utils/urls';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { A } from '@solidjs/router';
|
||||
|
||||
export const AuthLegalLinks: Component = () => {
|
||||
const { config } = useConfig();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
|
||||
export const NoAuthProviderWarning: Component = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div class="flex items-center justify-center h-full p-6 sm:pb-32">
|
||||
<div class="max-w-sm w-full">
|
||||
<h1 class="text-lg font-bold">{t('auth.no-auth-provider.title')}</h1>
|
||||
<p class="text-muted-foreground mt-1 mb-4">
|
||||
{t('auth.no-auth-provider.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Component, ComponentProps } from 'solid-js';
|
||||
import { splitProps } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { type Component, type ComponentProps, splitProps } from 'solid-js';
|
||||
|
||||
const providers = [
|
||||
{
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { createSignal, Match, Switch } from 'solid-js';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { createSignal } from 'solid-js';
|
||||
|
||||
export const SsoProviderButton: Component<{ name: string; icon: string; onClick: () => Promise<void>; label: string }> = (props) => {
|
||||
export const SsoProviderButton: Component<{ name: string; icon?: string; onClick: () => Promise<void>; label: string }> = (props) => {
|
||||
const [getIsLoading, setIsLoading] = createSignal(false);
|
||||
|
||||
const navigateToProvider = async () => {
|
||||
const onClick = async () => {
|
||||
setIsLoading(true);
|
||||
await props.onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="secondary" class="block w-full flex items-center justify-center" onClick={navigateToProvider} disabled={getIsLoading()}>
|
||||
<span class={cn(`mr-2 size-4.5 inline-block`, getIsLoading() ? 'i-tabler-loader-2 animate-spin' : props.icon)} />
|
||||
<Button variant="secondary" class="block w-full flex items-center justify-center gap-2" onClick={onClick} disabled={getIsLoading()}>
|
||||
|
||||
<Switch>
|
||||
<Match when={getIsLoading()}>
|
||||
<span class="i-tabler-loader-2 animate-spin" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.icon?.startsWith('i-')}>
|
||||
<span class={cn(`size-4.5`, props.icon)} />
|
||||
</Match>
|
||||
|
||||
<Match when={props.icon}>
|
||||
<img src={props.icon} alt={props.name} class="size-4.5" />
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
{props.label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { Navigate } from '@solidjs/router';
|
||||
import { type Component, Suspense } from 'solid-js';
|
||||
import { Suspense } from 'solid-js';
|
||||
import { Dynamic } from 'solid-js/web';
|
||||
import { match } from 'ts-pattern';
|
||||
import { useSession } from '../auth.services';
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { SsoProviderKey } from '../auth.types';
|
||||
import type { Component } from 'solid-js';
|
||||
import type { SsoProviderConfig } from '../auth.types';
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { createSignal, For, Show } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { createForm } from '@/modules/shared/form/form';
|
||||
@@ -6,13 +10,11 @@ import { Button } from '@/modules/ui/components/button';
|
||||
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
||||
import { Separator } from '@/modules/ui/components/separator';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { type Component, createSignal, For, Show } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||
import { getEnabledSsoProviderConfigs, isEmailVerificationRequiredError } from '../auth.models';
|
||||
import { signIn } from '../auth.services';
|
||||
import { authWithProvider, signIn } from '../auth.services';
|
||||
import { AuthLegalLinks } from '../components/legal-links.component';
|
||||
import { NoAuthProviderWarning } from '../components/no-auth-provider';
|
||||
import { SsoProviderButton } from '../components/sso-provider-button.component';
|
||||
|
||||
export const EmailLoginForm: Component = () => {
|
||||
@@ -85,9 +87,11 @@ export const EmailLoginForm: Component = () => {
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Button variant="link" as={A} class="inline p-0! h-auto" href="/request-password-reset">
|
||||
{t('auth.login.form.forgot-password.label')}
|
||||
</Button>
|
||||
<Show when={config.auth.isPasswordResetEnabled}>
|
||||
<Button variant="link" as={A} class="inline p-0! h-auto" href="/request-password-reset">
|
||||
{t('auth.login.form.forgot-password.label')}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Button type="submit" class="w-full">{t('auth.login.form.submit')}</Button>
|
||||
@@ -102,14 +106,18 @@ export const LoginPage: Component = () => {
|
||||
const { config } = useConfig();
|
||||
const { t } = useI18n();
|
||||
|
||||
const [getShowEmailLogin, setShowEmailLogin] = createSignal(false);
|
||||
const [getShowEmailLoginForm, setShowEmailLoginForm] = createSignal(false);
|
||||
|
||||
const loginWithProvider = async (provider: { key: SsoProviderKey }) => {
|
||||
await signIn.social({ provider: provider.key, callbackURL: config.baseUrl });
|
||||
const loginWithProvider = async (provider: SsoProviderConfig) => {
|
||||
await authWithProvider({ provider, config });
|
||||
};
|
||||
|
||||
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
|
||||
|
||||
if (!config.auth.providers.email.isEnabled && !getHasSsoProviders()) {
|
||||
return <AuthLayout><NoAuthProviderWarning /></AuthLayout>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div class="flex items-center justify-center h-full p-6 sm:pb-32">
|
||||
@@ -117,17 +125,22 @@ export const LoginPage: Component = () => {
|
||||
<h1 class="text-xl font-bold">{t('auth.login.title')}</h1>
|
||||
<p class="text-muted-foreground mt-1 mb-4">{t('auth.login.description')}</p>
|
||||
|
||||
{getShowEmailLogin() || !getHasSsoProviders()
|
||||
? <EmailLoginForm />
|
||||
: (
|
||||
<Button onClick={() => setShowEmailLogin(true)} class="w-full">
|
||||
<div class="i-tabler-mail mr-2 size-4.5" />
|
||||
{t('auth.login.login-with-provider', { provider: 'Email' })}
|
||||
</Button>
|
||||
)}
|
||||
<Show when={config.auth.providers.email.isEnabled}>
|
||||
{getShowEmailLoginForm() || !getHasSsoProviders()
|
||||
? <EmailLoginForm />
|
||||
: (
|
||||
<Button onClick={() => setShowEmailLoginForm(true)} class="w-full">
|
||||
<div class="i-tabler-mail mr-2 size-4.5" />
|
||||
{t('auth.login.login-with-provider', { provider: 'Email' })}
|
||||
</Button>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={config.auth.providers.email.isEnabled && getHasSsoProviders()}>
|
||||
<Separator class="my-4" />
|
||||
</Show>
|
||||
|
||||
<Show when={getHasSsoProviders()}>
|
||||
<Separator class="my-4" />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<For each={getEnabledSsoProviderConfigs({ config })}>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { ssoProviders } from '../auth.constants';
|
||||
import type { Component } from 'solid-js';
|
||||
import type { SsoProviderConfig } from '../auth.types';
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { createSignal, For, Show } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { createForm } from '@/modules/shared/form/form';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { Separator } from '@/modules/ui/components/separator';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { type Component, createSignal, For, Show } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||
import { getEnabledSsoProviderConfigs } from '../auth.models';
|
||||
import { signIn, signUp } from '../auth.services';
|
||||
import { authWithProvider, signUp } from '../auth.services';
|
||||
import { AuthLegalLinks } from '../components/legal-links.component';
|
||||
import { NoAuthProviderWarning } from '../components/no-auth-provider';
|
||||
import { SsoProviderButton } from '../components/sso-provider-button.component';
|
||||
|
||||
export const EmailRegisterForm: Component = () => {
|
||||
@@ -132,12 +134,16 @@ export const RegisterPage: Component = () => {
|
||||
|
||||
const [getShowEmailRegister, setShowEmailRegister] = createSignal(false);
|
||||
|
||||
const registerWithProvider = async (provider: typeof ssoProviders[number]) => {
|
||||
await signIn.social({ provider: provider.key });
|
||||
const registerWithProvider = async (provider: SsoProviderConfig) => {
|
||||
await authWithProvider({ provider, config });
|
||||
};
|
||||
|
||||
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
|
||||
|
||||
if (!config.auth.providers.email.isEnabled && !getHasSsoProviders()) {
|
||||
return <AuthLayout><NoAuthProviderWarning /></AuthLayout>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div class="flex items-center justify-center h-full p-6 sm:pb-32">
|
||||
@@ -149,17 +155,22 @@ export const RegisterPage: Component = () => {
|
||||
{t('auth.register.description')}
|
||||
</p>
|
||||
|
||||
{getShowEmailRegister() || !getHasSsoProviders()
|
||||
? <EmailRegisterForm />
|
||||
: (
|
||||
<Button onClick={() => setShowEmailRegister(true)} class="w-full">
|
||||
<div class="i-tabler-mail mr-2 size-4.5" />
|
||||
{t('auth.register.register-with-email')}
|
||||
</Button>
|
||||
)}
|
||||
<Show when={config.auth.providers.email.isEnabled}>
|
||||
{getShowEmailRegister() || !getHasSsoProviders()
|
||||
? <EmailRegisterForm />
|
||||
: (
|
||||
<Button onClick={() => setShowEmailRegister(true)} class="w-full">
|
||||
<div class="i-tabler-mail mr-2 size-4.5" />
|
||||
{t('auth.register.register-with-email')}
|
||||
</Button>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={config.auth.providers.email.isEnabled && getHasSsoProviders()}>
|
||||
<Separator class="my-4" />
|
||||
</Show>
|
||||
|
||||
<Show when={getHasSsoProviders()}>
|
||||
<Separator class="my-4" />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<For each={getEnabledSsoProviderConfigs({ config })}>
|
||||
@@ -168,7 +179,7 @@ export const RegisterPage: Component = () => {
|
||||
name={provider.name}
|
||||
icon={provider.icon}
|
||||
onClick={() => registerWithProvider(provider)}
|
||||
label={t('auth.register.register-with-provider', { provider: t(`auth.register.providers.${provider.key}`) })}
|
||||
label={t('auth.register.register-with-provider', { provider: provider.name })}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { buildUrl } from '@corentinth/chisels';
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { createForm } from '@/modules/shared/form/form';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { buildUrl } from '@corentinth/chisels';
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { type Component, createSignal, onMount } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||
import { forgetPassword } from '../auth.services';
|
||||
import { OpenEmailProvider } from '../components/open-email-provider.component';
|
||||
@@ -57,7 +58,7 @@ export const RequestPasswordResetPage: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
onMount(() => {
|
||||
if (config.auth.isPasswordResetEnabled) {
|
||||
if (!config.auth.isPasswordResetEnabled || !config.auth.providers.email.isEnabled) {
|
||||
navigate('/login');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { A, Navigate, useNavigate, useSearchParams } from '@solidjs/router';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { createForm } from '@/modules/shared/form/form';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { A, Navigate, useNavigate, useSearchParams } from '@solidjs/router';
|
||||
import { type Component, createSignal } from 'solid-js';
|
||||
import { onMount } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||
import { resetPassword } from '../auth.services';
|
||||
|
||||
@@ -62,7 +62,7 @@ export const ResetPasswordPage: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
onMount(() => {
|
||||
if (config.auth.isPasswordResetEnabled) {
|
||||
if (!config.auth.isPasswordResetEnabled || !config.auth.providers.email.isEnabled) {
|
||||
navigate('/login');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { debounce } from 'lodash-es';
|
||||
import { createContext, createEffect, createSignal, For, on, onCleanup, onMount, Show, useContext } from 'solid-js';
|
||||
import { getDocumentIcon } from '../documents/document.models';
|
||||
import { searchDocuments } from '../documents/documents.services';
|
||||
import { useI18n } from '../i18n/i18n.provider';
|
||||
import { cn } from '../shared/style/cn';
|
||||
import { useThemeStore } from '../theme/theme.store';
|
||||
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandLoading } from '../ui/components/command';
|
||||
@@ -29,9 +30,11 @@ export const CommandPaletteProvider: ParentComponent = (props) => {
|
||||
const [getIsCommandPaletteOpen, setIsCommandPaletteOpen] = createSignal(false);
|
||||
const [getMatchingDocuments, setMatchingDocuments] = createSignal<Document[]>([]);
|
||||
const [getSearchQuery, setSearchQuery] = createSignal('');
|
||||
const params = useParams();
|
||||
const [getIsLoading, setIsLoading] = createSignal(false);
|
||||
|
||||
const params = useParams();
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
@@ -82,7 +85,7 @@ export const CommandPaletteProvider: ParentComponent = (props) => {
|
||||
options: { label: string; icon: string; action: () => void; forceMatch?: boolean }[];
|
||||
}[] => [
|
||||
{
|
||||
label: 'Documents',
|
||||
label: t('command-palette.sections.documents'),
|
||||
forceMatch: true,
|
||||
options: getMatchingDocuments().map(document => ({
|
||||
label: document.name,
|
||||
@@ -92,20 +95,20 @@ export const CommandPaletteProvider: ParentComponent = (props) => {
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: `Theme`,
|
||||
label: t('command-palette.sections.theme'),
|
||||
options: [
|
||||
{
|
||||
label: 'Switch to light mode',
|
||||
label: t('layout.theme.light'),
|
||||
icon: 'i-tabler-sun',
|
||||
action: () => setColorMode({ mode: 'light' }),
|
||||
},
|
||||
{
|
||||
label: 'Switch to dark mode',
|
||||
label: t('layout.theme.dark'),
|
||||
icon: 'i-tabler-moon',
|
||||
action: () => setColorMode({ mode: 'dark' }),
|
||||
},
|
||||
{
|
||||
label: 'Switch to system',
|
||||
label: t('layout.theme.system'),
|
||||
icon: 'i-tabler-device-laptop',
|
||||
action: () => setColorMode({ mode: 'system' }),
|
||||
},
|
||||
@@ -132,7 +135,7 @@ export const CommandPaletteProvider: ParentComponent = (props) => {
|
||||
onOpenChange={setIsCommandPaletteOpen}
|
||||
>
|
||||
|
||||
<CommandInput onValueChange={setSearchQuery} placeholder="Search commands or documents" />
|
||||
<CommandInput onValueChange={setSearchQuery} placeholder={t('command-palette.search.placeholder')} />
|
||||
<CommandList>
|
||||
<Show when={getIsLoading()}>
|
||||
<CommandLoading>
|
||||
@@ -142,7 +145,7 @@ export const CommandPaletteProvider: ParentComponent = (props) => {
|
||||
<Show when={!getIsLoading()}>
|
||||
<Show when={getMatchingDocuments().length === 0}>
|
||||
<CommandEmpty>
|
||||
No results found.
|
||||
{t('command-palette.no-results')}
|
||||
</CommandEmpty>
|
||||
</Show>
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ParentComponent } from 'solid-js';
|
||||
import { createQuery } from '@tanstack/solid-query';
|
||||
import type { Config, RuntimePublicConfig } from './config';
|
||||
import { useQuery } from '@tanstack/solid-query';
|
||||
import { merge } from 'lodash-es';
|
||||
import { createContext, Match, Switch, useContext } from 'solid-js';
|
||||
import { Button } from '../ui/components/button';
|
||||
import { EmptyState } from '../ui/components/empty';
|
||||
import { createToast } from '../ui/components/sonner';
|
||||
import { buildTimeConfig, type Config, type RuntimePublicConfig } from './config';
|
||||
import { buildTimeConfig } from './config';
|
||||
import { fetchPublicConfig } from './config.services';
|
||||
|
||||
const ConfigContext = createContext<{
|
||||
@@ -23,7 +24,7 @@ export function useConfig() {
|
||||
}
|
||||
|
||||
export const ConfigProvider: ParentComponent = (props) => {
|
||||
const query = createQuery(() => ({
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['config'],
|
||||
queryFn: fetchPublicConfig,
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ const asString = <T extends string | undefined>(value: string | undefined, defau
|
||||
const asNumber = <T extends number | undefined>(value: string | undefined, defaultValue?: T): T extends undefined ? number | undefined : number => (value === undefined ? defaultValue : Number(value)) as T extends undefined ? number | undefined : number;
|
||||
|
||||
export const buildTimeConfig = {
|
||||
papraVersion: asString(import.meta.env.VITE_PAPRA_VERSION),
|
||||
papraVersion: asString(import.meta.env.VITE_PAPRA_VERSION, '0.0.0'),
|
||||
baseUrl: asString(import.meta.env.VITE_BASE_URL, window.location.origin),
|
||||
baseApiUrl: asString(import.meta.env.VITE_BASE_API_URL, window.location.origin),
|
||||
vitrineBaseUrl: asString(import.meta.env.VITE_VITRINE_BASE_URL, 'http://localhost:3000/'),
|
||||
@@ -16,8 +16,14 @@ export const buildTimeConfig = {
|
||||
isEmailVerificationRequired: asBoolean(import.meta.env.VITE_AUTH_IS_EMAIL_VERIFICATION_REQUIRED, true),
|
||||
showLegalLinksOnAuthPage: asBoolean(import.meta.env.VITE_AUTH_SHOW_LEGAL_LINKS_ON_AUTH_PAGE, false),
|
||||
providers: {
|
||||
email: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_EMAIL_IS_ENABLED, true) },
|
||||
github: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GITHUB_IS_ENABLED, false) },
|
||||
google: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GOOGLE_IS_ENABLED, false) },
|
||||
customs: [] as {
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
providerIconUrl: string;
|
||||
}[],
|
||||
},
|
||||
},
|
||||
documents: {
|
||||
@@ -30,8 +36,8 @@ export const buildTimeConfig = {
|
||||
},
|
||||
intakeEmails: {
|
||||
isEnabled: asBoolean(import.meta.env.VITE_INTAKE_EMAILS_IS_ENABLED, false),
|
||||
emailGenerationDomain: asString(import.meta.env.VITE_INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN),
|
||||
},
|
||||
isSubscriptionsEnabled: asBoolean(import.meta.env.VITE_IS_SUBSCRIPTIONS_ENABLED, false),
|
||||
} as const;
|
||||
|
||||
export type Config = typeof buildTimeConfig;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import type { HttpClientOptions, ResponseType } from '../shared/http/http-client';
|
||||
import { joinUrlPaths } from '@corentinth/chisels';
|
||||
|
||||
type ExtractRouteParams<Path extends string> =
|
||||
Path extends `${infer _Start}:${infer Param}/${infer Rest}`
|
||||
type ExtractRouteParams<Path extends string>
|
||||
= Path extends `${infer _Start}:${infer Param}/${infer Rest}`
|
||||
? { [k in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }
|
||||
: Path extends `${infer _Start}:${infer Param}`
|
||||
? { [k in Param]: string }
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import type { ApiKey } from '../api-keys/api-keys.types';
|
||||
import type { Webhook } from '../webhooks/webhooks.types';
|
||||
import { get } from 'lodash-es';
|
||||
import { FetchError } from 'ofetch';
|
||||
import { createRouter } from 'radix3';
|
||||
import { defineHandler } from './demo-api-mock.models';
|
||||
import { documentFileStorage, documentStorage, organizationStorage, tagDocumentStorage, taggingRuleStorage, tagStorage } from './demo.storage';
|
||||
import {
|
||||
apiKeyStorage,
|
||||
documentFileStorage,
|
||||
documentStorage,
|
||||
organizationStorage,
|
||||
tagDocumentStorage,
|
||||
taggingRuleStorage,
|
||||
tagStorage,
|
||||
webhooksStorage,
|
||||
} from './demo.storage';
|
||||
import { findMany, getValues } from './demo.storage.models';
|
||||
|
||||
const corpus = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
function randomString({ length = 10 }: { length?: number } = {}) {
|
||||
return Array.from({ length }, () => corpus[Math.floor(Math.random() * corpus.length)]).join('');
|
||||
}
|
||||
|
||||
function createId({ prefix }: { prefix: string }) {
|
||||
return `${prefix}_${randomString({ length: 24 })}`;
|
||||
}
|
||||
|
||||
function assert(condition: unknown, { message = 'Error', status }: { message?: string; status?: number } = {}): asserts condition {
|
||||
if (!condition) {
|
||||
throw Object.assign(new FetchError(message), { status });
|
||||
@@ -114,12 +135,13 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
assert(file, { status: 400 });
|
||||
|
||||
const document = {
|
||||
id: `doc_${Math.random().toString(36).slice(2)}`,
|
||||
id: createId({ prefix: 'doc' }),
|
||||
organizationId,
|
||||
name: file.name,
|
||||
originalName: file.name,
|
||||
originalSize: file.size,
|
||||
mimeType: file.type,
|
||||
content: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
tags: [],
|
||||
@@ -171,7 +193,7 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
const {
|
||||
pageIndex = 0,
|
||||
pageSize = 5,
|
||||
searchQuery = '',
|
||||
searchQuery: rawSearchQuery = '',
|
||||
} = query ?? {};
|
||||
|
||||
const organization = organizationStorage.getItem(organizationId);
|
||||
@@ -179,7 +201,9 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
|
||||
const documents = await findMany(documentStorage, document => document?.organizationId === organizationId);
|
||||
|
||||
const filteredDocuments = documents.filter(document => document?.name.includes(searchQuery) && !document?.deletedAt);
|
||||
const searchQuery = rawSearchQuery.trim().toLowerCase();
|
||||
|
||||
const filteredDocuments = documents.filter(document => document?.name.toLowerCase().includes(searchQuery) && !document?.deletedAt);
|
||||
|
||||
return {
|
||||
documents: filteredDocuments.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize),
|
||||
@@ -310,7 +334,7 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
assert(organization, { status: 403 });
|
||||
|
||||
const tag = {
|
||||
id: `tag_${Math.random().toString(36).slice(2)}`,
|
||||
id: createId({ prefix: 'tag' }),
|
||||
organizationId,
|
||||
name: get(body, 'name'),
|
||||
color: get(body, 'color'),
|
||||
@@ -372,7 +396,7 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
assert(tagId, { status: 400 });
|
||||
|
||||
const tagDocument = {
|
||||
id: `tagDoc_${Math.random().toString(36).slice(2)}`,
|
||||
id: createId({ prefix: 'tagDoc' }),
|
||||
tagId,
|
||||
documentId,
|
||||
createdAt: new Date(),
|
||||
@@ -411,7 +435,7 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
method: 'POST',
|
||||
handler: async ({ body }) => {
|
||||
const organization = {
|
||||
id: `org_${Math.random().toString(36).slice(2)}`,
|
||||
id: createId({ prefix: 'org' }),
|
||||
name: get(body, 'name'),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -475,7 +499,7 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
method: 'POST',
|
||||
handler: async ({ params: { organizationId }, body }) => {
|
||||
const taggingRule = {
|
||||
id: `tr_${Math.random().toString(36).slice(2)}`,
|
||||
id: createId({ prefix: 'tr' }),
|
||||
organizationId,
|
||||
name: get(body, 'name'),
|
||||
description: get(body, 'description'),
|
||||
@@ -544,6 +568,171 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
||||
await documentStorage.removeItem(key);
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/members',
|
||||
method: 'GET',
|
||||
handler: async ({ params: { organizationId } }) => {
|
||||
return {
|
||||
members: [{
|
||||
id: 'mem_1',
|
||||
user: {
|
||||
id: 'usr_1',
|
||||
email: 'jane.doe@papra.app',
|
||||
name: 'Jane Doe',
|
||||
},
|
||||
role: 'owner',
|
||||
organizationId,
|
||||
}],
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/members/invitations',
|
||||
method: 'POST',
|
||||
handler: async () => {
|
||||
throw Object.assign(new FetchError('Not available in demo'), {
|
||||
status: 501,
|
||||
data: {
|
||||
error: {
|
||||
message: 'This feature is not available in demo',
|
||||
code: 'demo.not_available',
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/members/me',
|
||||
method: 'GET',
|
||||
handler: async ({ params: { organizationId } }) => {
|
||||
return {
|
||||
member: {
|
||||
id: 'mem_1',
|
||||
role: 'owner',
|
||||
organizationId,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/api-keys',
|
||||
method: 'GET',
|
||||
handler: async () => {
|
||||
const apiKeys = await getValues(apiKeyStorage);
|
||||
|
||||
return { apiKeys };
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/api-keys',
|
||||
method: 'POST',
|
||||
handler: async ({ body }) => {
|
||||
const token = `ppapi_${randomString({ length: 64 })}`;
|
||||
|
||||
const apiKey = {
|
||||
id: createId({ prefix: 'apiKey' }),
|
||||
name: get(body, 'name'),
|
||||
permissions: get(body, 'permissions'),
|
||||
organizationIds: get(body, 'organizationIds'),
|
||||
allOrganizations: get(body, 'allOrganizations'),
|
||||
expiresAt: get(body, 'expiresAt'),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
prefix: token.slice(0, 11),
|
||||
} as ApiKey;
|
||||
|
||||
await apiKeyStorage.setItem(apiKey.id, apiKey);
|
||||
|
||||
return { apiKey, token };
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/api-keys/:apiKeyId',
|
||||
method: 'DELETE',
|
||||
handler: async ({ params: { apiKeyId } }) => {
|
||||
await apiKeyStorage.removeItem(apiKeyId);
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/invitations/count',
|
||||
method: 'GET',
|
||||
handler: async () => ({ pendingInvitationsCount: 0 }),
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/invitations',
|
||||
method: 'GET',
|
||||
handler: async () => ({ invitations: [] }),
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/webhooks',
|
||||
method: 'GET',
|
||||
handler: async ({ params: { organizationId } }) => {
|
||||
const webhooks = await findMany(webhooksStorage, webhook => webhook.organizationId === organizationId);
|
||||
|
||||
return { webhooks };
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/webhooks',
|
||||
method: 'POST',
|
||||
handler: async ({ params: { organizationId }, body }) => {
|
||||
const webhook: Webhook = {
|
||||
id: createId({ prefix: 'webhook' }),
|
||||
organizationId,
|
||||
name: get(body, 'name'),
|
||||
url: get(body, 'url'),
|
||||
enabled: true,
|
||||
events: get(body, 'events'),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await webhooksStorage.setItem(webhook.id, webhook);
|
||||
|
||||
return { webhook };
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/webhooks/:webhookId',
|
||||
method: 'GET',
|
||||
handler: async ({ params: { webhookId } }) => {
|
||||
const webhook = await webhooksStorage.getItem(webhookId);
|
||||
return { webhook };
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/webhooks/:webhookId',
|
||||
method: 'DELETE',
|
||||
handler: async ({ params: { webhookId } }) => {
|
||||
await webhooksStorage.removeItem(webhookId);
|
||||
},
|
||||
}),
|
||||
|
||||
...defineHandler({
|
||||
path: '/api/organizations/:organizationId/webhooks/:webhookId',
|
||||
method: 'PUT',
|
||||
handler: async ({ params: { webhookId }, body }) => {
|
||||
const webhook = await webhooksStorage.getItem(webhookId);
|
||||
|
||||
assert(webhook, { status: 404 });
|
||||
|
||||
await webhooksStorage.setItem(webhookId, Object.assign(webhook, body, { updatedAt: new Date() }));
|
||||
|
||||
return { webhook };
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const router = createRouter({ routes: inMemoryApiMock, strictTrailingSlash: false });
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { HttpClientOptions, ResponseType } from '../shared/http/http-client
|
||||
import { joinUrlPaths } from '@corentinth/chisels';
|
||||
import { router } from './demo-api-mock';
|
||||
|
||||
export async function demoHttpClient<A, R extends ResponseType = 'json'>(options: HttpClientOptions<R>): Promise<MappedResponseType< R, A>> {
|
||||
export async function demoHttpClient<A, R extends ResponseType = 'json'>(options: HttpClientOptions<R>): Promise<MappedResponseType<R, A>> {
|
||||
const path = `/${joinUrlPaths(options.method ?? 'GET', options.url)}`;
|
||||
const matchedRoute = router.lookup(path);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { type Component, createSignal } from 'solid-js';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { Portal } from 'solid-js/web';
|
||||
import { buildTimeConfig } from '../config/config';
|
||||
import { useI18n } from '../i18n/i18n.provider';
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function getValues<T extends StorageValue>(storage: Storage<T>): Pr
|
||||
return values;
|
||||
}
|
||||
|
||||
export async function findOne<T extends StorageValue>(storage: Storage<T>, predicate: (value: T) => boolean): Promise< T | null> {
|
||||
export async function findOne<T extends StorageValue>(storage: Storage<T>, predicate: (value: T) => boolean): Promise<T | null> {
|
||||
const values = await getValues(storage);
|
||||
const found = values.find(predicate);
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ApiKey } from '../api-keys/api-keys.types';
|
||||
import type { Document } from '../documents/documents.types';
|
||||
import type { Organization } from '../organizations/organizations.types';
|
||||
import type { TaggingRule } from '../tagging-rules/tagging-rules.types';
|
||||
import type { Tag } from '../tags/tags.types';
|
||||
import type { Webhook } from '../webhooks/webhooks.types';
|
||||
import { createStorage, prefixStorage } from 'unstorage';
|
||||
import localStorageDriver from 'unstorage/drivers/localstorage';
|
||||
import { trackingServices } from '../tracking/tracking.services';
|
||||
@@ -16,6 +18,8 @@ export const documentFileStorage = prefixStorage(storage, 'documentFiles');
|
||||
export const tagStorage = prefixStorage<Omit<Tag, 'documentsCount'>>(storage, 'tags');
|
||||
export const tagDocumentStorage = prefixStorage<{ documentId: string; tagId: string; id: string }>(storage, 'tagDocuments');
|
||||
export const taggingRuleStorage = prefixStorage<TaggingRule>(storage, 'taggingRules');
|
||||
export const apiKeyStorage = prefixStorage<ApiKey>(storage, 'apiKeys');
|
||||
export const webhooksStorage = prefixStorage<Webhook>(storage, 'webhooks');
|
||||
|
||||
export async function clearDemoStorage() {
|
||||
await storage.clear();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { ParentComponent } from 'solid-js';
|
||||
import type { Document } from '../documents.types';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { promptUploadFiles } from '@/modules/shared/files/upload';
|
||||
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { safely } from '@corentinth/chisels';
|
||||
import { A } from '@solidjs/router';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { createContext, createSignal, For, Match, Show, Switch, useContext } from 'solid-js';
|
||||
import { Portal } from 'solid-js/web';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { promptUploadFiles } from '@/modules/shared/files/upload';
|
||||
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { invalidateOrganizationDocumentsQuery } from '../documents.composables';
|
||||
import { uploadDocument } from '../documents.services';
|
||||
|
||||
@@ -17,7 +17,7 @@ const DocumentUploadContext = createContext<{
|
||||
uploadDocuments: (args: { files: File[]; organizationId: string }) => Promise<void>;
|
||||
}>();
|
||||
|
||||
export function useDocumentUpload({ organizationId }: { organizationId: string }) {
|
||||
export function useDocumentUpload({ getOrganizationId }: { getOrganizationId: () => string }) {
|
||||
const context = useContext(DocumentUploadContext);
|
||||
|
||||
if (!context) {
|
||||
@@ -27,11 +27,11 @@ export function useDocumentUpload({ organizationId }: { organizationId: string }
|
||||
const { uploadDocuments } = context;
|
||||
|
||||
return {
|
||||
uploadDocuments: async ({ files }: { files: File[] }) => uploadDocuments({ files, organizationId }),
|
||||
uploadDocuments: async ({ files }: { files: File[] }) => uploadDocuments({ files, organizationId: getOrganizationId() }),
|
||||
promptImport: async () => {
|
||||
const { files } = await promptUploadFiles();
|
||||
|
||||
await uploadDocuments({ files, organizationId });
|
||||
await uploadDocuments({ files, organizationId: getOrganizationId() });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -50,7 +50,7 @@ type TaskError = {
|
||||
|
||||
type Task = TaskSuccess | TaskError | {
|
||||
file: File;
|
||||
status: 'pending' | 'uploading' ;
|
||||
status: 'pending' | 'uploading';
|
||||
};
|
||||
|
||||
export const DocumentUploadProvider: ParentComponent = (props) => {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { DropdownMenuSubTriggerProps } from '@kobalte/core/dropdown-menu';
|
||||
import type { Component } from 'solid-js';
|
||||
import type { Document } from '../documents.types';
|
||||
import { A } from '@solidjs/router';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/modules/ui/components/dropdown-menu';
|
||||
import { A } from '@solidjs/router';
|
||||
import { useDeleteDocument } from '../documents.composables';
|
||||
import { useRenameDocumentDialog } from './rename-document-button.component';
|
||||
|
||||
export const DocumentManagementDropdown: Component<{ document: Document }> = (props) => {
|
||||
const { deleteDocument } = useDeleteDocument();
|
||||
const { openRenameDialog } = useRenameDocumentDialog();
|
||||
|
||||
const deleteDoc = () => deleteDocument({
|
||||
documentId: props.document.id,
|
||||
@@ -16,6 +18,7 @@ export const DocumentManagementDropdown: Component<{ document: Document }> = (pr
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
as={(props: DropdownMenuSubTriggerProps) => (
|
||||
@@ -34,6 +37,18 @@ export const DocumentManagementDropdown: Component<{ document: Document }> = (pr
|
||||
<span>Document details</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
class="cursor-pointer"
|
||||
onClick={() => openRenameDialog({
|
||||
documentId: props.document.id,
|
||||
organizationId: props.document.organizationId,
|
||||
documentName: props.document.name,
|
||||
})}
|
||||
>
|
||||
<div class="i-tabler-pencil size-4 mr-2"></div>
|
||||
<span>Rename document</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
class="cursor-pointer text-red"
|
||||
onClick={() => deleteDoc()}
|
||||
@@ -43,5 +58,6 @@ export const DocumentManagementDropdown: Component<{ document: Document }> = (pr
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import type { Document } from '../documents.types';
|
||||
import { useQuery } from '@tanstack/solid-query';
|
||||
import { createResource, Match, Suspense, Switch } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { Card } from '@/modules/ui/components/card';
|
||||
import { createQuery } from '@tanstack/solid-query';
|
||||
import { type Component, createResource, Match, Suspense, Switch } from 'solid-js';
|
||||
import { fetchDocumentFile } from '../documents.services';
|
||||
import { PdfViewer } from './pdf-viewer.component';
|
||||
|
||||
const imageMimeType = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
const pdfMimeType = ['application/pdf'];
|
||||
const txtLikeMimeType = ['text/plain', 'text/markdown', 'text/csv', 'text/html'];
|
||||
const txtLikeMimeType = ['application/x-yaml', 'application/json', 'application/xml'];
|
||||
|
||||
function blobToString(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -18,13 +20,90 @@ function blobToString(blob: Blob): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: IA generated code, add some tests
|
||||
* Detects if a blob can be safely displayed as text by checking for valid UTF-8 encoding
|
||||
* and common text patterns (low ratio of control characters, presence of readable text)
|
||||
*/
|
||||
async function isBlobTextSafe(blob: Blob): Promise<boolean> {
|
||||
try {
|
||||
const text = await blobToString(blob);
|
||||
|
||||
// Check if the text contains mostly printable characters
|
||||
const totalChars = text.length;
|
||||
if (totalChars === 0) {
|
||||
return true;
|
||||
} // Empty files are considered text-safe
|
||||
|
||||
// Count control characters (excluding common whitespace and newlines)
|
||||
// Use a simpler approach to avoid linter issues with Unicode escapes
|
||||
let controlCharCount = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const charCode = text.charCodeAt(i);
|
||||
// Check for control characters (0-31, 127-159) excluding common whitespace
|
||||
if ((charCode >= 0 && charCode <= 31 && ![9, 10, 13, 12, 11].includes(charCode))
|
||||
|| (charCode >= 127 && charCode <= 159)) {
|
||||
controlCharCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If more than 10% of characters are control characters, it's likely binary
|
||||
const controlCharRatio = controlCharCount / totalChars;
|
||||
if (controlCharRatio > 0.1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common binary file signatures in the first few bytes
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
// Common binary file signatures to check
|
||||
const binarySignatures = [
|
||||
[0xFF, 0xD8, 0xFF], // JPEG
|
||||
[0x89, 0x50, 0x4E, 0x47], // PNG
|
||||
[0x47, 0x49, 0x46], // GIF
|
||||
[0x25, 0x50, 0x44, 0x46], // PDF
|
||||
[0x50, 0x4B, 0x03, 0x04], // ZIP/DOCX/XLSX
|
||||
[0x7F, 0x45, 0x4C, 0x46], // ELF executable
|
||||
[0x4D, 0x5A], // Windows executable
|
||||
];
|
||||
|
||||
for (const signature of binarySignatures) {
|
||||
if (uint8Array.length >= signature.length) {
|
||||
const matches = signature.every((byte, index) => uint8Array[index] === byte);
|
||||
if (matches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the text contains mostly ASCII printable characters
|
||||
let asciiPrintableCount = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const charCode = text.charCodeAt(i);
|
||||
// ASCII printable characters (32-126) excluding common whitespace
|
||||
if (charCode >= 32 && charCode <= 126 && ![9, 10, 13, 12, 11].includes(charCode)) {
|
||||
asciiPrintableCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const asciiRatio = asciiPrintableCount / totalChars;
|
||||
|
||||
// If less than 70% are ASCII printable, it's likely binary
|
||||
return asciiRatio > 0.7;
|
||||
} catch {
|
||||
// If we can't read as text, it's definitely not text-safe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const TextFromBlob: Component<{ blob: Blob }> = (props) => {
|
||||
const [txt] = createResource(() => blobToString(props.blob));
|
||||
|
||||
return (
|
||||
<Card class="p-6 overflow-auto max-h-800px max-w-full text-xs">
|
||||
<Suspense>
|
||||
<pre>{txt()}</pre>
|
||||
<pre class="break-words whitespace-pre-wrap">{txt()}</pre>
|
||||
</Suspense>
|
||||
</Card>
|
||||
);
|
||||
@@ -33,12 +112,25 @@ const TextFromBlob: Component<{ blob: Blob }> = (props) => {
|
||||
export const DocumentPreview: Component<{ document: Document }> = (props) => {
|
||||
const getIsImage = () => imageMimeType.includes(props.document.mimeType);
|
||||
const getIsPdf = () => pdfMimeType.includes(props.document.mimeType);
|
||||
const getIsTxtLike = () => txtLikeMimeType.includes(props.document.mimeType) || props.document.mimeType.startsWith('text/');
|
||||
const { t } = useI18n();
|
||||
|
||||
const query = createQuery(() => ({
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['organizations', props.document.organizationId, 'documents', props.document.id, 'file'],
|
||||
queryFn: () => fetchDocumentFile({ documentId: props.document.id, organizationId: props.document.organizationId }),
|
||||
}));
|
||||
|
||||
// Create a resource to check if octet-stream blob is text-safe
|
||||
const [isOctetStreamTextSafe] = createResource(
|
||||
() => query.data && props.document.mimeType === 'application/octet-stream' ? query.data : null,
|
||||
async (blob) => {
|
||||
if (!blob) {
|
||||
return false;
|
||||
}
|
||||
return await isBlobTextSafe(blob);
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<Switch>
|
||||
@@ -47,12 +139,30 @@ export const DocumentPreview: Component<{ document: Document }> = (props) => {
|
||||
<img src={URL.createObjectURL(query.data!)} class="w-full h-full object-contain" />
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={getIsPdf() && query.data}>
|
||||
<PdfViewer url={URL.createObjectURL(query.data!)} />
|
||||
</Match>
|
||||
<Match when={txtLikeMimeType.includes(props.document.mimeType) && query.data}>
|
||||
|
||||
<Match when={getIsTxtLike() && query.data}>
|
||||
<TextFromBlob blob={query.data!} />
|
||||
</Match>
|
||||
|
||||
<Match when={props.document.mimeType === 'application/octet-stream' && query.data && isOctetStreamTextSafe()}>
|
||||
<TextFromBlob blob={query.data!} />
|
||||
</Match>
|
||||
|
||||
<Match when={props.document.mimeType === 'application/octet-stream' && query.data && !isOctetStreamTextSafe()}>
|
||||
<Card class="px-6 py-12 text-center text-sm text-muted-foreground">
|
||||
<p>{t('documents.preview.binary-file')}</p>
|
||||
</Card>
|
||||
</Match>
|
||||
|
||||
<Match when={query.data}>
|
||||
<Card class="px-6 py-12 text-center text-sm text-muted-foreground">
|
||||
<p>{t('documents.preview.unknown-file-type')}</p>
|
||||
</Card>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { promptUploadFiles } from '@/modules/shared/files/upload';
|
||||
import { queryClient } from '@/modules/shared/query/query-client';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { uploadDocument } from '../documents.services';
|
||||
|
||||
export const DocumentUploadArea: Component<{ organizationId?: string }> = (props) => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { Tag } from '@/modules/tags/tags.types';
|
||||
import type { TooltipTriggerProps } from '@kobalte/core/tooltip';
|
||||
import type { ColumnDef } from '@tanstack/solid-table';
|
||||
import type { Accessor, Component, Setter } from 'solid-js';
|
||||
import type { Document } from '../documents.types';
|
||||
import type { Tag } from '@/modules/tags/tags.types';
|
||||
import { formatBytes } from '@corentinth/chisels';
|
||||
import { A } from '@solidjs/router';
|
||||
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
|
||||
import { For, Match, Show, Switch } from 'solid-js';
|
||||
import { timeAgo } from '@/modules/shared/date/time-ago';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { TagLink } from '@/modules/tags/components/tag.component';
|
||||
@@ -9,11 +14,7 @@ import { Button } from '@/modules/ui/components/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/modules/ui/components/select';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/modules/ui/components/table';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/modules/ui/components/tooltip';
|
||||
import { formatBytes } from '@corentinth/chisels';
|
||||
import { A } from '@solidjs/router';
|
||||
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
|
||||
import { type Accessor, type Component, For, Match, type Setter, Show, Switch } from 'solid-js';
|
||||
import { getDocumentIcon } from '../document.models';
|
||||
import { getDocumentIcon, getDocumentNameExtension, getDocumentNameWithoutExtension } from '../document.models';
|
||||
import { DocumentManagementDropdown } from './document-management-dropdown.component';
|
||||
|
||||
type Pagination = {
|
||||
@@ -83,15 +84,11 @@ export const DocumentsPaginatedList: Component<{
|
||||
href={`/organizations/${data.row.original.organizationId}/documents/${data.row.original.id}`}
|
||||
class="font-bold truncate block hover:underline"
|
||||
>
|
||||
{data.row.original.name.split('.').slice(0, -1).join('.')}
|
||||
{getDocumentNameWithoutExtension({ name: data.row.original.name })}
|
||||
</A>
|
||||
|
||||
<div class="text-xs text-muted-foreground lh-tight">
|
||||
{formatBytes({ bytes: data.row.original.originalSize, base: 1000 })}
|
||||
{' '}
|
||||
-
|
||||
{' '}
|
||||
{data.row.original.name.split('.').pop()?.toUpperCase()}
|
||||
{[formatBytes({ bytes: data.row.original.originalSize, base: 1000 }), getDocumentNameExtension({ name: data.row.original.name })].filter(Boolean).join(' - ')}
|
||||
{' '}
|
||||
-
|
||||
{' '}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { createSignal, onCleanup } from 'solid-js';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
|
||||
export const GlobalDropArea: Component<{ onFilesDrop?: (args: { files: File[] }) => void }> = (props) => {
|
||||
const [isDragging, setIsDragging] = createSignal(false);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { Component, ParentComponent } from 'solid-js';
|
||||
import { setValue } from '@modular-forms/solid';
|
||||
import { useMutation } from '@tanstack/solid-query';
|
||||
import { createContext, createEffect, createSignal, useContext } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { createForm } from '@/modules/shared/form/form';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/modules/ui/components/dialog';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { getDocumentNameExtension, getDocumentNameWithoutExtension } from '../document.models';
|
||||
import { invalidateOrganizationDocumentsQuery } from '../documents.composables';
|
||||
import { updateDocument } from '../documents.services';
|
||||
|
||||
export const RenameDocumentDialog: Component<{
|
||||
documentId: string;
|
||||
organizationId: string;
|
||||
documentName: string;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}> = (props) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const renameDocumentMutation = useMutation(() => ({
|
||||
mutationFn: ({ name }: { name: string }) => updateDocument({ documentId: props.documentId, organizationId: props.organizationId, name }),
|
||||
onSuccess: async () => {
|
||||
createToast({
|
||||
message: t('documents.rename.success'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
props.setIsOpen(false);
|
||||
|
||||
await invalidateOrganizationDocumentsQuery({ organizationId: props.organizationId });
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const { Form, Field, form } = createForm({
|
||||
schema: v.object({
|
||||
name: v.pipe(
|
||||
v.string(),
|
||||
v.trim(),
|
||||
v.maxLength(255, t('documents.rename.form.name.max-length')),
|
||||
v.minLength(1, t('documents.rename.form.name.required')),
|
||||
),
|
||||
}),
|
||||
initialValues: {
|
||||
name: getDocumentNameWithoutExtension({ name: props.documentName }),
|
||||
},
|
||||
onSubmit: async ({ name }) => {
|
||||
const extension = getDocumentNameExtension({ name: props.documentName });
|
||||
const newName = extension ? `${name}.${extension}` : name;
|
||||
|
||||
await renameDocumentMutation.mutateAsync({ name: newName });
|
||||
},
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
setValue(form, 'name', getDocumentNameWithoutExtension({ name: props.documentName }));
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={props.setIsOpen} open={props.isOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('documents.rename.title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form>
|
||||
<Field name="name">
|
||||
{(field, inputProps) => (
|
||||
<TextFieldRoot>
|
||||
<TextFieldLabel class="sr-only" for="name">{t('documents.rename.form.name.label')}</TextFieldLabel>
|
||||
<TextField {...inputProps} value={field.value} id="name" placeholder={t('documents.rename.form.name.placeholder')} />
|
||||
{field.error && <div class="text-red-500 text-sm">{field.error}</div>}
|
||||
</TextFieldRoot>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div class="flex justify-end mt-4 gap-2">
|
||||
<Button type="button" variant="secondary" onClick={() => props.setIsOpen(false)}>
|
||||
{t('documents.rename.cancel')}
|
||||
</Button>
|
||||
<Button type="submit">{t('documents.rename.form.submit')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const context = createContext<{
|
||||
openRenameDialog: (args: { documentId: string; organizationId: string; documentName: string }) => void;
|
||||
}>();
|
||||
|
||||
export function useRenameDocumentDialog() {
|
||||
const renameDialogContext = useContext(context);
|
||||
|
||||
if (!renameDialogContext) {
|
||||
throw new Error('useRenameDocumentDialog must be used within a RenameDocumentDialogProvider');
|
||||
}
|
||||
|
||||
return renameDialogContext;
|
||||
}
|
||||
|
||||
export const RenameDocumentDialogProvider: ParentComponent = (props) => {
|
||||
const [getIsRenameDialogOpen, setIsRenameDialogOpen] = createSignal(false);
|
||||
const [getDocumentId, setDocumentId] = createSignal<string | undefined>(undefined);
|
||||
const [getOrganizationId, setOrganizationId] = createSignal<string | undefined>(undefined);
|
||||
const [getDocumentName, setDocumentName] = createSignal<string | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<context.Provider
|
||||
value={{
|
||||
openRenameDialog: ({ documentId, organizationId, documentName }) => {
|
||||
setIsRenameDialogOpen(true);
|
||||
setDocumentId(documentId);
|
||||
setOrganizationId(organizationId);
|
||||
setDocumentName(documentName);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RenameDocumentDialog
|
||||
documentId={getDocumentId() ?? ''}
|
||||
organizationId={getOrganizationId() ?? ''}
|
||||
documentName={getDocumentName() ?? ''}
|
||||
isOpen={getIsRenameDialogOpen()}
|
||||
setIsOpen={setIsRenameDialogOpen}
|
||||
/>
|
||||
|
||||
{props.children}
|
||||
</context.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { icons as tablerIconSet } from '@iconify-json/tabler';
|
||||
import { values } from 'lodash-es';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getDaysBeforePermanentDeletion, getDocumentIcon, iconByFileType } from './document.models';
|
||||
import { getDaysBeforePermanentDeletion, getDocumentIcon, getDocumentNameExtension, getDocumentNameWithoutExtension, iconByFileType } from './document.models';
|
||||
|
||||
describe('files models', () => {
|
||||
describe('iconByFileType', () => {
|
||||
@@ -101,4 +101,24 @@ describe('files models', () => {
|
||||
expect(daysBeforeDeletion).to.eql(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocumentNameWithoutExtension', () => {
|
||||
test('get the document name without the extension', () => {
|
||||
expect(getDocumentNameWithoutExtension({ name: 'document.txt' })).to.eql('document');
|
||||
expect(getDocumentNameWithoutExtension({ name: 'document' })).to.eql('document');
|
||||
expect(getDocumentNameWithoutExtension({ name: '.document' })).to.eql('.document');
|
||||
expect(getDocumentNameWithoutExtension({ name: '.document.txt' })).to.eql('.document');
|
||||
expect(getDocumentNameWithoutExtension({ name: 'document.test.txt' })).to.eql('document.test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocumentNameExtension', () => {
|
||||
test('get the document name extension', () => {
|
||||
expect(getDocumentNameExtension({ name: 'document.txt' })).to.eql('txt');
|
||||
expect(getDocumentNameExtension({ name: 'document' })).to.eql(undefined);
|
||||
expect(getDocumentNameExtension({ name: '.document' })).to.eql(undefined);
|
||||
expect(getDocumentNameExtension({ name: '.document.txt' })).to.eql('txt');
|
||||
expect(getDocumentNameExtension({ name: 'document.test.txt' })).to.eql('txt');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DocumentActivityEvent } from './documents.types';
|
||||
import { addDays, differenceInDays } from 'date-fns';
|
||||
|
||||
export const iconByFileType = {
|
||||
@@ -49,3 +50,46 @@ export function getDaysBeforePermanentDeletion({ document, deletedDocumentsReten
|
||||
|
||||
return daysBeforeDeletion;
|
||||
}
|
||||
|
||||
export function getDocumentNameWithoutExtension({ name }: { name: string }) {
|
||||
const dotSplittedName = name.split('.');
|
||||
const dotCount = dotSplittedName.length - 1;
|
||||
|
||||
if (dotCount === 0) {
|
||||
return name;
|
||||
}
|
||||
|
||||
if (dotCount === 1 && name.startsWith('.')) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return dotSplittedName.slice(0, -1).join('.');
|
||||
}
|
||||
|
||||
export function getDocumentNameExtension({ name }: { name: string }) {
|
||||
const dotSplittedName = name.split('.');
|
||||
const dotCount = dotSplittedName.length - 1;
|
||||
|
||||
if (dotCount === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (dotCount === 1 && name.startsWith('.')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return dotSplittedName[dotCount];
|
||||
}
|
||||
|
||||
export const documentActivityIcon: Record<DocumentActivityEvent, string> = {
|
||||
created: 'i-tabler-file-plus',
|
||||
updated: 'i-tabler-file-diff',
|
||||
deleted: 'i-tabler-file-x',
|
||||
restored: 'i-tabler-file-check',
|
||||
tagged: 'i-tabler-tag',
|
||||
untagged: 'i-tabler-tag-off',
|
||||
} as const;
|
||||
|
||||
export function getDocumentActivityIcon({ event }: { event: DocumentActivityEvent }) {
|
||||
return documentActivityIcon[event] ?? 'i-tabler-file';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const DOCUMENT_ACTIVITY_EVENTS = {
|
||||
CREATED: 'created',
|
||||
UPDATED: 'updated',
|
||||
DELETED: 'deleted',
|
||||
RESTORED: 'restored',
|
||||
TAGGED: 'tagged',
|
||||
UNTAGGED: 'untagged',
|
||||
} as const;
|
||||
|
||||
export const DOCUMENT_ACTIVITY_EVENT_LIST = Object.values(DOCUMENT_ACTIVITY_EVENTS);
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Document } from './documents.types';
|
||||
import type { AsDto } from '../shared/http/http-client.types';
|
||||
import type { Document, DocumentActivity } from './documents.types';
|
||||
import { apiClient } from '../shared/http/api-client';
|
||||
import { getFormData } from '../shared/http/http-client.models';
|
||||
import { coerceDates, getFormData } from '../shared/http/http-client.models';
|
||||
|
||||
export async function uploadDocument({
|
||||
file,
|
||||
@@ -9,18 +10,14 @@ export async function uploadDocument({
|
||||
file: File;
|
||||
organizationId: string;
|
||||
}) {
|
||||
const { document } = await apiClient<{ document: Document }>({
|
||||
const { document } = await apiClient<{ document: AsDto<Document> }>({
|
||||
method: 'POST',
|
||||
path: `/api/organizations/${organizationId}/documents`,
|
||||
body: getFormData({ file }),
|
||||
});
|
||||
|
||||
return {
|
||||
document: {
|
||||
...document,
|
||||
createdAt: new Date(document.createdAt),
|
||||
updatedAt: document.updatedAt ? new Date(document.updatedAt) : undefined,
|
||||
},
|
||||
document: coerceDates(document),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +37,7 @@ export async function fetchOrganizationDocuments({
|
||||
const {
|
||||
documents,
|
||||
documentsCount,
|
||||
} = await apiClient<{ documents: Document[]; documentsCount: number }>({
|
||||
} = await apiClient<{ documents: AsDto<Document>[]; documentsCount: number }>({
|
||||
method: 'GET',
|
||||
path: `/api/organizations/${organizationId}/documents`,
|
||||
query: {
|
||||
@@ -52,11 +49,7 @@ export async function fetchOrganizationDocuments({
|
||||
|
||||
return {
|
||||
documentsCount,
|
||||
documents: documents.map(document => ({
|
||||
...document,
|
||||
createdAt: new Date(document.createdAt),
|
||||
updatedAt: document.updatedAt ? new Date(document.updatedAt) : undefined,
|
||||
})),
|
||||
documents: documents.map(coerceDates),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +65,7 @@ export async function fetchOrganizationDeletedDocuments({
|
||||
const {
|
||||
documents,
|
||||
documentsCount,
|
||||
} = await apiClient<{ documents: Document[]; documentsCount: number }>({
|
||||
} = await apiClient<{ documents: AsDto<Document>[]; documentsCount: number }>({
|
||||
method: 'GET',
|
||||
path: `/api/organizations/${organizationId}/documents/deleted`,
|
||||
query: {
|
||||
@@ -83,11 +76,7 @@ export async function fetchOrganizationDeletedDocuments({
|
||||
|
||||
return {
|
||||
documentsCount,
|
||||
documents: documents.map(document => ({
|
||||
...document,
|
||||
createdAt: new Date(document.createdAt),
|
||||
updatedAt: document.updatedAt ? new Date(document.updatedAt) : undefined,
|
||||
})),
|
||||
documents: documents.map(coerceDates),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,17 +113,13 @@ export async function fetchDocument({
|
||||
documentId: string;
|
||||
organizationId: string;
|
||||
}) {
|
||||
const { document } = await apiClient<{ document: Document }>({
|
||||
const { document } = await apiClient<{ document: AsDto<Document> }>({
|
||||
method: 'GET',
|
||||
path: `/api/organizations/${organizationId}/documents/${documentId}`,
|
||||
});
|
||||
|
||||
return {
|
||||
document: {
|
||||
...document,
|
||||
createdAt: new Date(document.createdAt),
|
||||
updatedAt: document.updatedAt ? new Date(document.updatedAt) : undefined,
|
||||
},
|
||||
document: coerceDates(document),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,7 +152,7 @@ export async function searchDocuments({
|
||||
}) {
|
||||
const {
|
||||
documents,
|
||||
} = await apiClient<{ documents: Document[] }>({
|
||||
} = await apiClient<{ documents: AsDto<Document>[] }>({
|
||||
method: 'GET',
|
||||
path: `/api/organizations/${organizationId}/documents/search`,
|
||||
query: {
|
||||
@@ -178,11 +163,7 @@ export async function searchDocuments({
|
||||
});
|
||||
|
||||
return {
|
||||
documents: documents.map(document => ({
|
||||
...document,
|
||||
createdAt: new Date(document.createdAt),
|
||||
updatedAt: document.updatedAt ? new Date(document.updatedAt) : undefined,
|
||||
})),
|
||||
documents: documents.map(coerceDates),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -208,3 +189,50 @@ export async function deleteTrashDocument({ documentId, organizationId }: { docu
|
||||
path: `/api/organizations/${organizationId}/documents/trash/${documentId}`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateDocument({
|
||||
documentId,
|
||||
organizationId,
|
||||
content,
|
||||
name,
|
||||
}: {
|
||||
documentId: string;
|
||||
organizationId: string;
|
||||
content?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
const { document } = await apiClient<{ document: AsDto<Document> }>({
|
||||
method: 'PATCH',
|
||||
path: `/api/organizations/${organizationId}/documents/${documentId}`,
|
||||
body: { content, name },
|
||||
});
|
||||
|
||||
return {
|
||||
document: coerceDates(document),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchDocumentActivities({
|
||||
documentId,
|
||||
organizationId,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}: {
|
||||
documentId: string;
|
||||
organizationId: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
}) {
|
||||
const { activities } = await apiClient<{ activities: AsDto<DocumentActivity>[] }>({
|
||||
method: 'GET',
|
||||
path: `/api/organizations/${organizationId}/documents/${documentId}/activity`,
|
||||
query: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
activities: activities.map(coerceDates),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Tag } from '../tags/tags.types';
|
||||
import type { User } from '../users/users.types';
|
||||
import type { DOCUMENT_ACTIVITY_EVENTS } from './documents.constants';
|
||||
|
||||
export type Document = {
|
||||
id: string;
|
||||
@@ -11,5 +13,20 @@ export type Document = {
|
||||
isDeleted?: boolean;
|
||||
deletedAt?: Date;
|
||||
deletedBy?: string;
|
||||
content: string;
|
||||
tags: Tag[];
|
||||
};
|
||||
|
||||
export type DocumentActivityEvent = (typeof DOCUMENT_ACTIVITY_EVENTS)[keyof typeof DOCUMENT_ACTIVITY_EVENTS];
|
||||
|
||||
export type DocumentActivity = {
|
||||
id: string;
|
||||
documentId: string;
|
||||
event: DocumentActivityEvent;
|
||||
eventData: Record<string, unknown>;
|
||||
userId?: string;
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
tag?: Pick<Tag, 'id' | 'name' | 'color' | 'description'>;
|
||||
user?: Pick<User, 'id' | 'name'>;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import type { Document } from '../documents.types';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import { keepPreviousData, useMutation, useQuery } from '@tanstack/solid-query';
|
||||
import { createSignal, Show, Suspense } from 'solid-js';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||
@@ -7,15 +11,13 @@ import { queryClient } from '@/modules/shared/query/query-client';
|
||||
import { Alert, AlertDescription } from '@/modules/ui/components/alert';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import { createMutation, createQuery, keepPreviousData } from '@tanstack/solid-query';
|
||||
import { type Component, createSignal, Show, Suspense } from 'solid-js';
|
||||
import { DocumentsPaginatedList } from '../components/documents-list.component';
|
||||
import { useRestoreDocument } from '../documents.composables';
|
||||
import { deleteAllTrashDocuments, deleteTrashDocument, fetchOrganizationDeletedDocuments } from '../documents.services';
|
||||
|
||||
const RestoreDocumentButton: Component<{ document: Document }> = (props) => {
|
||||
const { getIsRestoring, restore } = useRestoreDocument();
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -25,11 +27,11 @@ const RestoreDocumentButton: Component<{ document: Document }> = (props) => {
|
||||
isLoading={getIsRestoring()}
|
||||
>
|
||||
{ getIsRestoring()
|
||||
? (<>Restoring...</>)
|
||||
? (<>{t('documents.deleted.restoring')}</>)
|
||||
: (
|
||||
<>
|
||||
<div class="i-tabler-refresh size-4 mr-2" />
|
||||
Restore
|
||||
{t('documents.actions.restore')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -40,7 +42,7 @@ const PermanentlyDeleteTrashDocumentButton: Component<{ document: Document; orga
|
||||
const { confirm } = useConfirmModal();
|
||||
const { t } = useI18n();
|
||||
|
||||
const deleteMutation = createMutation(() => ({
|
||||
const deleteMutation = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
await deleteTrashDocument({ documentId: props.document.id, organizationId: props.organizationId });
|
||||
},
|
||||
@@ -81,7 +83,7 @@ const PermanentlyDeleteTrashDocumentButton: Component<{ document: Document; orga
|
||||
class="text-red-500 hover:text-red-600"
|
||||
>
|
||||
{deleteMutation.isPending
|
||||
? (<>Deleting...</>)
|
||||
? (<>{t('documents.deleted.deleting')}</>)
|
||||
: (
|
||||
<>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
@@ -96,7 +98,7 @@ const DeleteAllTrashDocumentsButton: Component<{ organizationId: string }> = (pr
|
||||
const { confirm } = useConfirmModal();
|
||||
const { t } = useI18n();
|
||||
|
||||
const deleteAllMutation = createMutation(() => ({
|
||||
const deleteAllMutation = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
await deleteAllTrashDocuments({ organizationId: props.organizationId });
|
||||
},
|
||||
@@ -132,7 +134,7 @@ const DeleteAllTrashDocumentsButton: Component<{ organizationId: string }> = (pr
|
||||
class="text-red-500 hover:text-red-600"
|
||||
>
|
||||
{deleteAllMutation.isPending
|
||||
? (<>Deleting...</>)
|
||||
? (<>{t('documents.deleted.deleting')}</>)
|
||||
: (
|
||||
<>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
@@ -147,8 +149,9 @@ export const DeletedDocumentsPage: Component = () => {
|
||||
const [getPagination, setPagination] = createSignal({ pageIndex: 0, pageSize: 100 });
|
||||
const params = useParams();
|
||||
const { config } = useConfig();
|
||||
const { t } = useI18n();
|
||||
|
||||
const query = createQuery(() => ({
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['organizations', params.organizationId, 'documents', 'deleted', getPagination()],
|
||||
queryFn: () => fetchOrganizationDeletedDocuments({
|
||||
organizationId: params.organizationId,
|
||||
@@ -159,16 +162,12 @@ export const DeletedDocumentsPage: Component = () => {
|
||||
|
||||
return (
|
||||
<div class="p-6 mt-4 pb-32">
|
||||
<h1 class="text-2xl font-bold">Deleted documents</h1>
|
||||
<h1 class="text-2xl font-bold">{t('documents.deleted.title')}</h1>
|
||||
|
||||
<Alert variant="muted" class="my-4 flex items-center gap-6 xl:gap-4">
|
||||
<div class="i-tabler-info-circle size-10 xl:size-8 text-primary flex-shrink-0 hidden sm:block" />
|
||||
<AlertDescription>
|
||||
All deleted documents are stored in the trash bin for
|
||||
{' '}
|
||||
{config.documents.deletedDocumentsRetentionDays}
|
||||
{' '}
|
||||
days. Passing this delay, the documents will be permanently deleted, and you will not be able to restore them.
|
||||
{t('documents.deleted.retention-notice', { days: config.documents.deletedDocumentsRetentionDays })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -176,13 +175,9 @@ export const DeletedDocumentsPage: Component = () => {
|
||||
<Show when={query.data?.documents.length === 0}>
|
||||
<div class="flex flex-col items-center justify-center gap-2 pt-24 mx-auto max-w-md text-center">
|
||||
<div class="i-tabler-trash text-primary size-12" aria-hidden="true" />
|
||||
<div class="text-xl font-medium">No deleted documents</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
You have no deleted documents. Documents that are deleted will be moved to the trash bin for
|
||||
{' '}
|
||||
{config.documents.deletedDocumentsRetentionDays}
|
||||
{' '}
|
||||
days.
|
||||
<div class="text-xl font-medium">{t('documents.deleted.empty.title')}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{t('documents.deleted.empty.description', { days: config.documents.deletedDocumentsRetentionDays })}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -202,7 +197,7 @@ export const DeletedDocumentsPage: Component = () => {
|
||||
id: 'deletion',
|
||||
cell: data => (
|
||||
<div class="text-muted-foreground hidden sm:block">
|
||||
Deleted
|
||||
{t('documents.deleted.deleted-at')}
|
||||
{' '}
|
||||
<span class="text-foreground font-bold" title={data.row.original.deletedAt?.toLocaleString()}>{timeAgo({ date: data.row.original.deletedAt! })}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import type { Component, JSX } from 'solid-js';
|
||||
import type { DocumentActivity } from '../documents.types';
|
||||
import { formatBytes, safely } from '@corentinth/chisels';
|
||||
import { A, useNavigate, useParams, useSearchParams } from '@solidjs/router';
|
||||
import { createQueries, useInfiniteQuery } from '@tanstack/solid-query';
|
||||
import { createEffect, createSignal, For, Match, Show, Suspense, Switch } from 'solid-js';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { timeAgo } from '@/modules/shared/date/time-ago';
|
||||
import { downloadFile } from '@/modules/shared/files/download';
|
||||
import { queryClient } from '@/modules/shared/query/query-client';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { DocumentTagPicker } from '@/modules/tags/components/tag-picker.component';
|
||||
import { TagLink } from '@/modules/tags/components/tag.component';
|
||||
import { CreateTagModal } from '@/modules/tags/pages/tags.page';
|
||||
import { addTagToDocument, removeTagFromDocument } from '@/modules/tags/tags.services';
|
||||
import { Alert } from '@/modules/ui/components/alert';
|
||||
import { Alert, AlertDescription } from '@/modules/ui/components/alert';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { Separator } from '@/modules/ui/components/separator';
|
||||
import { formatBytes } from '@corentinth/chisels';
|
||||
import { useNavigate, useParams } from '@solidjs/router';
|
||||
import { createQueries } from '@tanstack/solid-query';
|
||||
import { type Component, For, type JSX, Show, Suspense } from 'solid-js';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
import { Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger } from '@/modules/ui/components/tabs';
|
||||
import { TextArea } from '@/modules/ui/components/textarea';
|
||||
import { TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { DocumentPreview } from '../components/document-preview.component';
|
||||
import { getDaysBeforePermanentDeletion } from '../document.models';
|
||||
import { useRenameDocumentDialog } from '../components/rename-document-button.component';
|
||||
import { getDaysBeforePermanentDeletion, getDocumentActivityIcon } from '../document.models';
|
||||
import { useDeleteDocument, useRestoreDocument } from '../documents.composables';
|
||||
import { fetchDocument, fetchDocumentFile } from '../documents.services';
|
||||
import { fetchDocument, fetchDocumentActivities, fetchDocumentFile, updateDocument } from '../documents.services';
|
||||
import '@pdfslick/solid/dist/pdf_viewer.css';
|
||||
|
||||
type KeyValueItem = {
|
||||
@@ -29,7 +41,7 @@ const KeyValues: Component<{ data?: KeyValueItem[] }> = (props) => {
|
||||
<For each={props.data}>
|
||||
{item => (
|
||||
<tr>
|
||||
<td class="py-1 pr-2 text-sm text-muted-foreground flex items-center gap-2">
|
||||
<td class="py-1 pr-2 text-sm text-muted-foreground flex items-center gap-2 whitespace-nowrap">
|
||||
{item.icon && <div class={item.icon}></div>}
|
||||
{item.label}
|
||||
</td>
|
||||
@@ -42,12 +54,78 @@ const KeyValues: Component<{ data?: KeyValueItem[] }> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentPage: Component = () => {
|
||||
const ActivityItem: Component<{ activity: DocumentActivity }> = (props) => {
|
||||
const { t, te } = useI18n();
|
||||
const params = useParams();
|
||||
|
||||
return (
|
||||
<div class="border-b py-3 flex items-center gap-2">
|
||||
<div>
|
||||
<div class={cn(getDocumentActivityIcon({ event: props.activity.event }), 'size-6 text-muted-foreground')} />
|
||||
</div>
|
||||
<div>
|
||||
<Switch fallback={<span class="text-sm">{t(`activity.document.${props.activity.event}`)}</span>}>
|
||||
<Match when={['tagged', 'untagged'].includes(props.activity.event)}>
|
||||
<span class="text-sm flex items-baseline gap-1">
|
||||
{te(`activity.document.${props.activity.event}`, { tag: props.activity.tag ? <TagLink {...props.activity.tag} organizationId={params.organizationId} class="text-xs" /> : undefined })}
|
||||
</span>
|
||||
</Match>
|
||||
|
||||
<Match when={props.activity.event === 'updated' && (props.activity.eventData.updatedFields as string[]).length === 1}>
|
||||
<span class="text-sm flex items-baseline gap-1">
|
||||
{te(`activity.document.updated.single`, {
|
||||
field: <span class="font-bold">{(props.activity.eventData.updatedFields as string[])[0]}</span>,
|
||||
})}
|
||||
</span>
|
||||
</Match>
|
||||
|
||||
<Match when={props.activity.event === 'updated' && (props.activity.eventData.updatedFields as string[]).length > 1}>
|
||||
<span class="text-sm flex items-baseline gap-1">
|
||||
{te(`activity.document.updated.multiple`, { fields: (props.activity.eventData.updatedFields as string[]).join(', ') })}
|
||||
</span>
|
||||
</Match>
|
||||
|
||||
</Switch>
|
||||
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<span title={props.activity.createdAt.toLocaleString()}>{timeAgo({ date: props.activity.createdAt })}</span>
|
||||
<Show when={props.activity.user}>
|
||||
{getUser => (
|
||||
<span>{te('activity.document.user.name', { name: <A href={`/organizations/${params.organizationId}/members`} class="underline hover:text-primary transition">{getUser().name}</A> })}</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tabs = ['info', 'content', 'activity'] as const;
|
||||
type Tab = typeof tabs[number];
|
||||
|
||||
export const DocumentPage: Component = () => {
|
||||
const { t } = useI18n();
|
||||
const params = useParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { deleteDocument } = useDeleteDocument();
|
||||
const { restore, getIsRestoring } = useRestoreDocument();
|
||||
const navigate = useNavigate();
|
||||
const { config } = useConfig();
|
||||
const { openRenameDialog } = useRenameDocumentDialog();
|
||||
|
||||
const getInitialTab = (): Tab => {
|
||||
const tab = searchParams.tab;
|
||||
if (tab && typeof tab === 'string' && tabs.includes(tab as Tab)) {
|
||||
return tab as Tab;
|
||||
}
|
||||
return 'info';
|
||||
};
|
||||
|
||||
const [getTab, setTab] = createSignal<Tab>(getInitialTab());
|
||||
|
||||
createEffect(() => {
|
||||
setSearchParams({ tab: getTab() }, { replace: true });
|
||||
});
|
||||
|
||||
const queries = createQueries(() => ({
|
||||
queries: [
|
||||
@@ -62,6 +140,30 @@ export const DocumentPage: Component = () => {
|
||||
],
|
||||
}));
|
||||
|
||||
const activityPageSize = 20;
|
||||
const activityQuery = useInfiniteQuery(() => ({
|
||||
enabled: getTab() === 'activity',
|
||||
queryKey: ['organizations', params.organizationId, 'documents', params.documentId, 'activity'],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
const { activities } = await fetchDocumentActivities({
|
||||
documentId: params.documentId,
|
||||
organizationId: params.organizationId,
|
||||
pageIndex: pageParam,
|
||||
pageSize: activityPageSize,
|
||||
});
|
||||
|
||||
return activities;
|
||||
},
|
||||
getNextPageParam: (lastPage, _pages, lastPageParam) => {
|
||||
if (lastPage.length < activityPageSize) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return lastPageParam + 1;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
}));
|
||||
|
||||
const deleteDoc = async () => {
|
||||
if (!queries[0].data) {
|
||||
return;
|
||||
@@ -82,6 +184,44 @@ export const DocumentPage: Component = () => {
|
||||
|
||||
const getDataUrl = () => queries[1].data ? URL.createObjectURL(queries[1].data) : undefined;
|
||||
|
||||
const [isEditing, setIsEditing] = createSignal(false);
|
||||
const [editedContent, setEditedContent] = createSignal('');
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
const handleEdit = () => {
|
||||
setEditedContent(queries[0].data?.document.content ?? '');
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
setEditedContent('');
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!queries[0].data?.document) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
const [, error] = await safely(updateDocument({
|
||||
documentId: queries[0].data.document.id,
|
||||
organizationId: params.organizationId,
|
||||
content: editedContent(),
|
||||
}));
|
||||
setIsSaving(false);
|
||||
setIsEditing(false);
|
||||
|
||||
if (error) {
|
||||
createToast({ type: 'error', message: 'Failed to update document content' });
|
||||
return;
|
||||
}
|
||||
createToast({ type: 'success', message: 'Document content updated' });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['organizations', params.organizationId, 'documents', params.documentId],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="p-6 flex gap-6 h-full flex-col md:flex-row max-w-7xl mx-auto">
|
||||
<Suspense>
|
||||
@@ -90,7 +230,21 @@ export const DocumentPage: Component = () => {
|
||||
{getDocument => (
|
||||
<div class="flex gap-4 md:pr-6">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-xl font-semibold">{getDocument().name}</h1>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex items-center gap-2 group bg-transparent! px-0 text-left h-auto"
|
||||
onClick={() => openRenameDialog({
|
||||
documentId: getDocument().id,
|
||||
organizationId: params.organizationId,
|
||||
documentName: getDocument().name,
|
||||
})}
|
||||
>
|
||||
<h1 class="text-xl font-semibold lh-tight" title={getDocument().name}>
|
||||
{getDocument().name}
|
||||
</h1>
|
||||
|
||||
<div class="i-tabler-pencil size-4 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0"></div>
|
||||
</Button>
|
||||
<p class="text-sm text-muted-foreground mb-6">{getDocument().id}</p>
|
||||
|
||||
<div class="flex gap-2 mb-2">
|
||||
@@ -100,7 +254,7 @@ export const DocumentPage: Component = () => {
|
||||
size="sm"
|
||||
>
|
||||
<div class="i-tabler-download size-4 mr-2"></div>
|
||||
Download
|
||||
{t('documents.actions.download')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -109,7 +263,7 @@ export const DocumentPage: Component = () => {
|
||||
size="sm"
|
||||
>
|
||||
<div class="i-tabler-eye size-4 mr-2"></div>
|
||||
Open in new tab
|
||||
{t('documents.actions.open-in-new-tab')}
|
||||
</Button>
|
||||
|
||||
{getDocument().isDeleted
|
||||
@@ -121,7 +275,7 @@ export const DocumentPage: Component = () => {
|
||||
isLoading={getIsRestoring()}
|
||||
>
|
||||
<div class="i-tabler-refresh size-4 mr-2"></div>
|
||||
Restore
|
||||
{t('documents.actions.restore')}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
@@ -131,79 +285,191 @@ export const DocumentPage: Component = () => {
|
||||
onClick={deleteDoc}
|
||||
>
|
||||
<div class="i-tabler-trash size-4 mr-2"></div>
|
||||
Delete
|
||||
{t('documents.actions.delete')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DocumentTagPicker
|
||||
organizationId={params.organizationId}
|
||||
tagIds={getDocument().tags.map(tag => tag.id)}
|
||||
onTagAdded={async ({ tag }) => {
|
||||
await addTagToDocument({
|
||||
documentId: params.documentId,
|
||||
organizationId: params.organizationId,
|
||||
tagId: tag.id,
|
||||
});
|
||||
}}
|
||||
<div class="flex gap-2 sm:items-center sm:flex-row flex-col">
|
||||
<div class="flex-1">
|
||||
<DocumentTagPicker
|
||||
organizationId={params.organizationId}
|
||||
tagIds={getDocument().tags.map(tag => tag.id)}
|
||||
onTagAdded={async ({ tag }) => {
|
||||
await addTagToDocument({
|
||||
documentId: params.documentId,
|
||||
organizationId: params.organizationId,
|
||||
tagId: tag.id,
|
||||
});
|
||||
}}
|
||||
|
||||
onTagRemoved={async ({ tag }) => {
|
||||
await removeTagFromDocument({
|
||||
documentId: params.documentId,
|
||||
organizationId: params.organizationId,
|
||||
tagId: tag.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
onTagRemoved={async ({ tag }) => {
|
||||
await removeTagFromDocument({
|
||||
documentId: params.documentId,
|
||||
organizationId: params.organizationId,
|
||||
tagId: tag.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateTagModal organizationId={params.organizationId}>
|
||||
{params => (
|
||||
<Button variant="outline" {...params}>
|
||||
<div class="i-tabler-plus size-4 mr-2"></div>
|
||||
{t('tagging-rules.form.tags.add-tag')}
|
||||
</Button>
|
||||
)}
|
||||
</CreateTagModal>
|
||||
</div>
|
||||
|
||||
{getDocument().isDeleted && (
|
||||
<Alert variant="destructive" class="mt-6">
|
||||
This document has been deleted and will be permanently removed in
|
||||
{' '}
|
||||
{getDaysBeforePermanentDeletion({
|
||||
{t('documents.deleted.message', { days: getDaysBeforePermanentDeletion({
|
||||
document: getDocument(),
|
||||
deletedDocumentsRetentionDays: config.documents.deletedDocumentsRetentionDays,
|
||||
})}
|
||||
{' '}
|
||||
days.
|
||||
}) ?? 0 })}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Separator class="my-6" />
|
||||
<Separator class="my-3" />
|
||||
|
||||
<Tabs value={getTab()} onChange={setTab} class="w-full">
|
||||
<TabsList class="w-full h-8">
|
||||
<TabsTrigger value="info">{t('documents.tabs.info')}</TabsTrigger>
|
||||
<TabsTrigger value="content">{t('documents.tabs.content')}</TabsTrigger>
|
||||
<TabsTrigger value="activity">{t('documents.tabs.activity')}</TabsTrigger>
|
||||
<TabsIndicator />
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="info">
|
||||
<KeyValues data={[
|
||||
{
|
||||
label: t('documents.info.id'),
|
||||
value: getDocument().id,
|
||||
icon: 'i-tabler-id',
|
||||
},
|
||||
{
|
||||
label: t('documents.info.name'),
|
||||
value: (
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex items-center gap-2 group bg-transparent! p-0 h-auto text-left"
|
||||
onClick={() => openRenameDialog({
|
||||
documentId: getDocument().id,
|
||||
organizationId: params.organizationId,
|
||||
documentName: getDocument().name,
|
||||
})}
|
||||
>
|
||||
{getDocument().name}
|
||||
|
||||
<div class="i-tabler-pencil size-4 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0"></div>
|
||||
</Button>
|
||||
),
|
||||
icon: 'i-tabler-file-text',
|
||||
},
|
||||
{
|
||||
label: t('documents.info.type'),
|
||||
value: getDocument().mimeType,
|
||||
icon: 'i-tabler-file-unknown',
|
||||
},
|
||||
{
|
||||
label: t('documents.info.size'),
|
||||
value: formatBytes({ bytes: getDocument().originalSize, base: 1000 }),
|
||||
icon: 'i-tabler-weight',
|
||||
},
|
||||
{
|
||||
label: t('documents.info.created-at'),
|
||||
value: timeAgo({ date: getDocument().createdAt }),
|
||||
icon: 'i-tabler-calendar',
|
||||
},
|
||||
{
|
||||
label: t('documents.info.updated-at'),
|
||||
value: getDocument().updatedAt ? timeAgo({ date: getDocument().updatedAt! }) : <span class="text-muted-foreground">{t('documents.info.never')}</span>,
|
||||
icon: 'i-tabler-calendar',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="content">
|
||||
<Show
|
||||
when={isEditing()}
|
||||
fallback={(
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="whitespace-pre-wrap font-mono text-sm bg-muted p-4 rounded-md max-h-[400px] overflow-auto">
|
||||
{queries[0].data?.document.content}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button variant="outline" onClick={handleEdit}>
|
||||
<div class="i-tabler-edit size-4 mr-2" />
|
||||
{t('documents.actions.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Alert variant="muted" class="my-4 flex items-center gap-2">
|
||||
<div class="i-tabler-info-circle size-8 flex-shrink-0" />
|
||||
<AlertDescription>
|
||||
{t('documents.content.alert')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<TextFieldRoot>
|
||||
<TextArea
|
||||
value={editedContent()}
|
||||
onInput={e => setEditedContent(e.currentTarget.value)}
|
||||
class="font-mono min-h-[200px]"
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={handleCancel} disabled={isSaving()}>
|
||||
{t('documents.actions.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving()}>
|
||||
{isSaving() ? t('documents.actions.saving') : t('documents.actions.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</TabsContent>
|
||||
<TabsContent value="activity">
|
||||
<Show when={activityQuery.data?.pages}>
|
||||
{getActivitiesPages => (
|
||||
<div class="flex flex-col">
|
||||
<For each={getActivitiesPages() ?? []}>
|
||||
{activities => (
|
||||
<For each={activities}>
|
||||
{activity => (
|
||||
<ActivityItem activity={activity} />
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show
|
||||
when={activityQuery.hasNextPage}
|
||||
fallback={(
|
||||
<div class="text-sm text-muted-foreground text-center py-4">
|
||||
{t('activity.no-more-activities')}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => activityQuery.fetchNextPage()}
|
||||
isLoading={activityQuery.isFetchingNextPage}
|
||||
>
|
||||
{t('activity.load-more')}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<KeyValues data={[
|
||||
{
|
||||
label: 'ID',
|
||||
value: getDocument().id,
|
||||
icon: 'i-tabler-id',
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
value: getDocument().name,
|
||||
icon: 'i-tabler-file-text',
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
value: getDocument().mimeType,
|
||||
icon: 'i-tabler-file-unknown',
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
value: formatBytes({ bytes: getDocument().originalSize, base: 1000 }),
|
||||
icon: 'i-tabler-weight',
|
||||
},
|
||||
{
|
||||
label: 'Created At',
|
||||
value: timeAgo({ date: getDocument().createdAt }),
|
||||
icon: 'i-tabler-calendar',
|
||||
},
|
||||
{
|
||||
label: 'Updated At',
|
||||
value: getDocument().updatedAt ? timeAgo({ date: getDocument().updatedAt! }) : <span class="text-muted-foreground">Never</span>,
|
||||
icon: 'i-tabler-calendar',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { fetchOrganization } from '@/modules/organizations/organizations.services';
|
||||
import { Tag } from '@/modules/tags/components/tag.component';
|
||||
import { fetchTags } from '@/modules/tags/tags.services';
|
||||
import type { Component } from 'solid-js';
|
||||
import { useParams, useSearchParams } from '@solidjs/router';
|
||||
import { createQueries, keepPreviousData } from '@tanstack/solid-query';
|
||||
import { castArray } from 'lodash-es';
|
||||
import { type Component, createSignal, For, Show, Suspense } from 'solid-js';
|
||||
import { createSignal, For, Show, Suspense } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { fetchOrganization } from '@/modules/organizations/organizations.services';
|
||||
import { Tag } from '@/modules/tags/components/tag.component';
|
||||
import { fetchTags } from '@/modules/tags/tags.services';
|
||||
import { DocumentUploadArea } from '../components/document-upload-area.component';
|
||||
import { createdAtColumn, DocumentsPaginatedList, standardActionsColumn, tagsColumn } from '../components/documents-list.component';
|
||||
import { fetchOrganizationDocuments } from '../documents.services';
|
||||
|
||||
export const DocumentsPage: Component = () => {
|
||||
const params = useParams();
|
||||
const { t } = useI18n();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [getPagination, setPagination] = createSignal({ pageIndex: 0, pageSize: 100 });
|
||||
|
||||
@@ -50,11 +53,11 @@ export const DocumentsPage: Component = () => {
|
||||
? (
|
||||
<>
|
||||
<h2 class="text-xl font-bold ">
|
||||
No documents
|
||||
{t('documents.list.no-documents.title')}
|
||||
</h2>
|
||||
|
||||
<p class="text-muted-foreground mt-1 mb-6">
|
||||
There are no documents in this organization yet. Start by uploading some documents.
|
||||
{t('documents.list.no-documents.description')}
|
||||
</p>
|
||||
|
||||
<DocumentUploadArea />
|
||||
@@ -64,7 +67,7 @@ export const DocumentsPage: Component = () => {
|
||||
: (
|
||||
<>
|
||||
<h2 class="text-lg font-semibold mb-4">
|
||||
Documents
|
||||
{t('documents.list.title')}
|
||||
</h2>
|
||||
<Show when={hasFilters()}>
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
@@ -82,7 +85,7 @@ export const DocumentsPage: Component = () => {
|
||||
|
||||
<Show when={hasFilters() && query[0].data?.documentsCount === 0}>
|
||||
<p class="text-muted-foreground mt-1 mb-6">
|
||||
No documents found
|
||||
{t('documents.list.no-results')}
|
||||
</p>
|
||||
</Show>
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export const locales = [
|
||||
{ key: 'en', name: 'English' },
|
||||
{ key: 'fr', name: 'Français' },
|
||||
];
|
||||
{ key: 'de', name: 'Deutsch' },
|
||||
{ key: 'pt-BR', name: 'Português Brasileiro' },
|
||||
{ key: 'pt', name: 'Português Europeu' },
|
||||
{ key: 'pl', name: 'Polski' },
|
||||
{ key: 'ro', name: 'Română' },
|
||||
{ key: 'es', name: 'Español' },
|
||||
] as const;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Accessor, ParentComponent, Setter } from 'solid-js';
|
||||
import type { LocaleKeys } from './locales.types';
|
||||
import { makePersisted } from '@solid-primitives/storage';
|
||||
import { createContext, createEffect, createResource, createSignal, Show, useContext } from 'solid-js';
|
||||
import defaultDict from '../../locales/en.yml?flattened';
|
||||
import defaultDict from '../../locales/en.yml';
|
||||
import { locales } from './i18n.constants';
|
||||
import { createFragmentTranslator, createTranslator, findMatchingLocale } from './i18n.models';
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useI18n() {
|
||||
}
|
||||
|
||||
async function fetchDictionary(locale: Locale): Promise<Dictionary> {
|
||||
const { default: dict } = await import(`../../locales/${locale}.yml?flattened`);
|
||||
const { default: dict } = await import(`../../locales/${locale}.yml`);
|
||||
|
||||
return {
|
||||
...defaultDict,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { glob } from 'tinyglobby';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
const rawLocales = import.meta.glob('../../locales/*.yml', { eager: true, query: '?flattened' });
|
||||
const { en: defaultLocal, ...locales } = Object.fromEntries(
|
||||
const rawLocales = import.meta.glob('../../locales/*.yml', { eager: true });
|
||||
const locales = Object.fromEntries(
|
||||
Object.entries(rawLocales).map(([key, value]: [string, any]) => [key.replace('../../locales/', '').replace('.yml', ''), value.default]),
|
||||
);
|
||||
|
||||
const { en: defaultLocal } = locales;
|
||||
|
||||
describe('locales', () => {
|
||||
for (const [locale, translations] of Object.entries(locales)) {
|
||||
describe(locale, () => {
|
||||
@@ -26,4 +30,41 @@ describe('locales', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('all keys in en.yml must be used in the app (dynamic keys are manually excluded)', async () => {
|
||||
const srcFileNames = await glob(['src/**/*.{ts,tsx}', '!src/**/*.test.*', '!src/modules/i18n/locales.types.ts'], { cwd: process.cwd() });
|
||||
|
||||
// Exclude keys that are used in dynamic contexts
|
||||
const dynamicKeysMatchers = [
|
||||
/^api-errors\./, // api-errors.document.already_exists
|
||||
/^auth\.register\.providers\.[a-z0-9:]+$/, // auth.register.providers.google
|
||||
/^webhooks\.events\.documents\.[a-z0-9:]+.description$/, // webhooks.events.organization.organization:created
|
||||
/^api-keys\.permissions\.[a-z0-9:]+\.[a-z0-9:]+$/, // api-keys.permissions.documents.documents:delete
|
||||
/^organizations\.members\.roles\.[a-z0-9]+$/, // organizations.members.roles.admin
|
||||
/^activity\.document\.[a-z0-9:]+$/, // activity.document.created
|
||||
/^organizations\.invitations\.status\.[a-z0-9:]+$/, // organizations.invitations.status.pending
|
||||
];
|
||||
|
||||
const keys = new Set(
|
||||
Object
|
||||
.keys(defaultLocal)
|
||||
.filter(key => !dynamicKeysMatchers.some(matcher => matcher.test(key))),
|
||||
);
|
||||
|
||||
for (const srcFileName of srcFileNames) {
|
||||
const fileContent = await readFile(srcFileName, 'utf-8');
|
||||
|
||||
for (const key of keys) {
|
||||
if (fileContent.includes(key)) {
|
||||
keys.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (keys.size === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect([...keys]).to.eql([], 'Unused keys found in en.yml, please remove them (or add them to the dynamic keys matchers in locales.test.ts if they are used in dynamic contexts)');
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,10 @@
|
||||
import type { AsDto } from '../shared/http/http-client.types';
|
||||
import type { IntakeEmail } from './intake-emails.types';
|
||||
import { apiClient } from '../shared/http/api-client';
|
||||
import { coerceDates } from '../shared/http/http-client.models';
|
||||
|
||||
export async function fetchIntakeEmails({ organizationId }: { organizationId: string }) {
|
||||
const { intakeEmails } = await apiClient<{ intakeEmails: IntakeEmail[] }>({
|
||||
const { intakeEmails } = await apiClient<{ intakeEmails: AsDto<IntakeEmail>[] }>({
|
||||
path: `/api/organizations/${organizationId}/intake-emails`,
|
||||
method: 'GET',
|
||||
});
|
||||
@@ -14,7 +15,7 @@ export async function fetchIntakeEmails({ organizationId }: { organizationId: st
|
||||
}
|
||||
|
||||
export async function createIntakeEmail({ organizationId }: { organizationId: string }) {
|
||||
const { intakeEmail } = await apiClient<{ intakeEmail: IntakeEmail }>({
|
||||
const { intakeEmail } = await apiClient<{ intakeEmail: AsDto<IntakeEmail> }>({
|
||||
path: `/api/organizations/${organizationId}/intake-emails`,
|
||||
method: 'POST',
|
||||
});
|
||||
@@ -42,7 +43,7 @@ export async function updateIntakeEmail({
|
||||
isEnabled?: boolean;
|
||||
allowedOrigins?: string[];
|
||||
}) {
|
||||
const { intakeEmail } = await apiClient<{ intakeEmail: IntakeEmail }>({
|
||||
const { intakeEmail } = await apiClient<{ intakeEmail: AsDto<IntakeEmail> }>({
|
||||
path: `/api/organizations/${organizationId}/intake-emails/${intakeEmailId}`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
|
||||
@@ -6,5 +6,5 @@ export type IntakeEmail = {
|
||||
allowedOrigins: string[];
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date | undefined;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { DialogTriggerProps } from '@kobalte/core/dialog';
|
||||
import type { Component, JSX } from 'solid-js';
|
||||
import type { IntakeEmail } from '../intake-emails.types';
|
||||
import { safely } from '@corentinth/chisels';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import { useQuery } from '@tanstack/solid-query';
|
||||
import { createSignal, For, Show, Suspense } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { useConfig } from '@/modules/config/config.provider';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||
import { createForm } from '@/modules/shared/form/form';
|
||||
import { isHttpErrorWithCode } from '@/modules/shared/http/http-errors';
|
||||
@@ -13,16 +20,11 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
|
||||
import { EmptyState } from '@/modules/ui/components/empty';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
import { safely } from '@corentinth/chisels';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import { createQuery } from '@tanstack/solid-query';
|
||||
import { type Component, For, type JSX, Show, Suspense } from 'solid-js';
|
||||
import { createSignal } from 'solid-js';
|
||||
import * as v from 'valibot';
|
||||
import { createIntakeEmail, deleteIntakeEmail, fetchIntakeEmails, updateIntakeEmail } from '../intake-emails.services';
|
||||
|
||||
const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) => JSX.Element; intakeEmails: IntakeEmail }> = (props) => {
|
||||
const [getAllowedOrigins, setAllowedOrigins] = createSignal([...props.intakeEmails.allowedOrigins]);
|
||||
const { t } = useI18n();
|
||||
|
||||
const update = async () => {
|
||||
await updateIntakeEmail({
|
||||
@@ -47,7 +49,7 @@ const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) =>
|
||||
}),
|
||||
onSubmit: async ({ email }) => {
|
||||
if (getAllowedOrigins().includes(email)) {
|
||||
throw new Error('This email is already in the allowed origins for this intake email');
|
||||
throw new Error(t('intake-emails.allowed-origins.add.error.exists'));
|
||||
}
|
||||
|
||||
setAllowedOrigins(origins => [...origins, email]);
|
||||
@@ -67,13 +69,9 @@ const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) =>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Allowed origins</DialogTitle>
|
||||
<DialogTitle>{t('intake-emails.allowed-origins.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Only emails sent to
|
||||
{' '}
|
||||
<span class="font-medium text-primary">{props.intakeEmails.emailAddress}</span>
|
||||
{' '}
|
||||
from these origins will be processed. If no origins are specified, all emails will be discarded.
|
||||
{t('intake-emails.allowed-origins.description', { email: props.intakeEmails.emailAddress })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -81,13 +79,13 @@ const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) =>
|
||||
<Field name="email">
|
||||
{(field, inputProps) => (
|
||||
<TextFieldRoot class="flex flex-col gap-1 mb-4 mt-4">
|
||||
<TextFieldLabel for="email">Add allowed origin email</TextFieldLabel>
|
||||
<TextFieldLabel for="email">{t('intake-emails.allowed-origins.add.label')}</TextFieldLabel>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<TextField type="email" id="email" placeholder="Eg. ada@papra.app" {...inputProps} autoFocus value={field.value} aria-invalid={Boolean(field.error)} />
|
||||
<TextField type="email" id="email" placeholder={t('intake-emails.allowed-origins.add.placeholder')} {...inputProps} autoFocus value={field.value} aria-invalid={Boolean(field.error)} />
|
||||
<Button type="submit">
|
||||
<div class="i-tabler-plus size-4 mr-2" />
|
||||
Add
|
||||
{t('intake-emails.allowed-origins.add.button')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -130,22 +128,39 @@ const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) =>
|
||||
|
||||
export const IntakeEmailsPage: Component = () => {
|
||||
const { config } = useConfig();
|
||||
const { t, te } = useI18n();
|
||||
|
||||
if (!config.intakeEmails.isEnabled) {
|
||||
return (
|
||||
<Card class="p-6">
|
||||
<h2 class="text-base font-bold">Intake Emails</h2>
|
||||
<div class="p-6 max-w-screen-md mx-auto mt-10">
|
||||
<h1 class="text-xl font-semibold">{t('intake-emails.title')}</h1>
|
||||
|
||||
<p class="text-muted-foreground mt-1">
|
||||
Intake emails are disabled on this instance. Please contact your administrator to enable them.
|
||||
{t('intake-emails.description')}
|
||||
</p>
|
||||
</Card>
|
||||
<Card class="px-6 py-4 mt-4 flex items-center gap-4">
|
||||
<div class="i-tabler-mail-off size-12 text-muted-foreground flex-shrink-0" />
|
||||
<div>
|
||||
<h2 class="text-base font-bold text-muted-foreground">{t('intake-emails.disabled.title')}</h2>
|
||||
<p class="text-muted-foreground mt-1">
|
||||
{te('intake-emails.disabled.description', {
|
||||
documentation: (
|
||||
<a href="https://docs.papra.app/guides/intake-emails-with-owlrelay/" target="_blank" class="text-primary">
|
||||
{t('intake-emails.disabled.documentation')}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const params = useParams();
|
||||
const { confirm } = useConfirmModal();
|
||||
|
||||
const query = createQuery(() => ({
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['organizations', params.organizationId, 'intake-emails'],
|
||||
queryFn: () => fetchIntakeEmails({ organizationId: params.organizationId }),
|
||||
}));
|
||||
@@ -155,7 +170,7 @@ export const IntakeEmailsPage: Component = () => {
|
||||
|
||||
if (isHttpErrorWithCode({ error, code: 'intake_email.limit_reached' })) {
|
||||
createToast({
|
||||
message: 'The maximum number of intake emails for this organization has been reached. Please upgrade your plan to create more intake emails.',
|
||||
message: t('api-errors.intake_email.limit_reached'),
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
@@ -169,20 +184,20 @@ export const IntakeEmailsPage: Component = () => {
|
||||
await query.refetch();
|
||||
|
||||
createToast({
|
||||
message: 'Intake email created',
|
||||
message: t('intake-emails.create.success'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const deleteEmail = async ({ intakeEmailId }: { intakeEmailId: string }) => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete intake email?',
|
||||
message: 'Are you sure you want to delete this intake email? This action cannot be undone.',
|
||||
title: t('intake-emails.delete.confirm.title'),
|
||||
message: t('intake-emails.delete.confirm.message'),
|
||||
cancelButton: {
|
||||
text: 'Cancel',
|
||||
text: t('intake-emails.delete.confirm.cancel-button'),
|
||||
},
|
||||
confirmButton: {
|
||||
text: 'Delete intake email',
|
||||
text: t('intake-emails.delete.confirm.confirm-button'),
|
||||
variant: 'destructive',
|
||||
},
|
||||
});
|
||||
@@ -195,7 +210,7 @@ export const IntakeEmailsPage: Component = () => {
|
||||
await query.refetch();
|
||||
|
||||
createToast({
|
||||
message: 'Intake email deleted',
|
||||
message: t('intake-emails.delete.success'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
@@ -205,27 +220,25 @@ export const IntakeEmailsPage: Component = () => {
|
||||
await query.refetch();
|
||||
|
||||
createToast({
|
||||
message: `Intake email ${isEnabled ? 'enabled' : 'disabled'}`,
|
||||
message: isEnabled ? t('intake-emails.update.success.enabled') : t('intake-emails.update.success.disabled'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card class="p-6">
|
||||
|
||||
<h2 class="text-base font-bold">Intake Emails</h2>
|
||||
<div class="p-6 max-w-screen-md mx-auto mt-10">
|
||||
<h1 class="text-xl font-semibold">{t('intake-emails.title')}</h1>
|
||||
|
||||
<p class="text-muted-foreground mt-1">
|
||||
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.
|
||||
{t('intake-emails.description')}
|
||||
</p>
|
||||
|
||||
<Alert variant="default" class="mt-4 flex items-center gap-4 xl:gap-4 text-muted-foreground">
|
||||
<div class="i-tabler-info-circle size-10 xl:size-8 text-primary flex-shrink-0 " />
|
||||
|
||||
<AlertDescription>
|
||||
Only enabled intake emails from allowed origins will be processed. You can enable or disable an intake email at any time.
|
||||
{t('intake-emails.info')}
|
||||
</AlertDescription>
|
||||
|
||||
</Alert>
|
||||
|
||||
<Suspense>
|
||||
@@ -236,14 +249,14 @@ export const IntakeEmailsPage: Component = () => {
|
||||
fallback={(
|
||||
<div class="mt-4 py-8 border-2 border-dashed rounded-lg text-center">
|
||||
<EmptyState
|
||||
title="No intake emails"
|
||||
description="Generate an intake address to easily ingest emails attachments."
|
||||
title={t('intake-emails.empty.title')}
|
||||
description={t('intake-emails.empty.description')}
|
||||
class="pt-0"
|
||||
icon="i-tabler-mail"
|
||||
cta={(
|
||||
<Button variant="secondary" onClick={createEmail}>
|
||||
<div class="i-tabler-plus size-4 mr-2" />
|
||||
Generate intake email
|
||||
{t('intake-emails.empty.generate')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
@@ -252,19 +265,22 @@ export const IntakeEmailsPage: Component = () => {
|
||||
>
|
||||
<div class="mt-4 mb-4 flex items-center justify-between">
|
||||
<div class="text-muted-foreground">
|
||||
{`${intakeEmails().length} intake email${intakeEmails().length > 1 ? 's' : ''} for this organization`}
|
||||
{t('intake-emails.count', {
|
||||
count: intakeEmails().length,
|
||||
plural: intakeEmails().length > 1 ? 's' : '',
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button onClick={createEmail}>
|
||||
<div class="i-tabler-plus size-4 mr-2" />
|
||||
New intake email
|
||||
{t('intake-emails.new')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<For each={intakeEmails()}>
|
||||
{intakeEmail => (
|
||||
<div class="flex items-center justify-between border rounded-lg p-4">
|
||||
<div class="flex items-center justify-between border rounded-lg p-4 bg-card">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="bg-muted size-9 rounded-lg flex items-center justify-center">
|
||||
<div class={cn('i-tabler-mail size-5', intakeEmail.isEnabled ? 'text-primary' : 'text-muted-foreground')} />
|
||||
@@ -275,9 +291,8 @@ export const IntakeEmailsPage: Component = () => {
|
||||
{intakeEmail.emailAddress}
|
||||
|
||||
<Show when={!intakeEmail.isEnabled}>
|
||||
<span class="text-muted-foreground text-xs ml-2">(Disabled)</span>
|
||||
<span class="text-muted-foreground text-xs ml-2">{t('intake-emails.disabled-label')}</span>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
|
||||
<Show
|
||||
@@ -285,14 +300,16 @@ export const IntakeEmailsPage: Component = () => {
|
||||
fallback={(
|
||||
<div class="text-xs text-warning flex items-center gap-1.5">
|
||||
<div class="i-tabler-alert-triangle size-3.75" />
|
||||
No allowed email origins
|
||||
{t('intake-emails.no-origins')}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-2">
|
||||
{`Allowed from ${intakeEmail.allowedOrigins.length} address${intakeEmail.allowedOrigins.length > 1 ? 'es' : ''}`}
|
||||
{t('intake-emails.allowed-origins', {
|
||||
count: intakeEmail.allowedOrigins.length,
|
||||
plural: intakeEmail.allowedOrigins.length > 1 ? 'es' : '',
|
||||
})}
|
||||
</div>
|
||||
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,7 +320,7 @@ export const IntakeEmailsPage: Component = () => {
|
||||
onClick={() => updateEmail({ intakeEmailId: intakeEmail.id, isEnabled: !intakeEmail.isEnabled })}
|
||||
>
|
||||
<div class="i-tabler-power size-4 mr-2" />
|
||||
{intakeEmail.isEnabled ? 'Disable' : 'Enable'}
|
||||
{intakeEmail.isEnabled ? t('intake-emails.actions.disable') : t('intake-emails.actions.enable')}
|
||||
</Button>
|
||||
|
||||
<AllowedOriginsDialog intakeEmails={intakeEmail}>
|
||||
@@ -315,7 +332,7 @@ export const IntakeEmailsPage: Component = () => {
|
||||
class="flex items-center gap-2 leading-none"
|
||||
>
|
||||
<div class="i-tabler-edit size-4" />
|
||||
Manage origins addresses
|
||||
{t('intake-emails.actions.manage-origins')}
|
||||
</Button>
|
||||
)}
|
||||
</AllowedOriginsDialog>
|
||||
@@ -327,21 +344,17 @@ export const IntakeEmailsPage: Component = () => {
|
||||
class="text-red"
|
||||
>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
|
||||
Delete
|
||||
{t('intake-emails.actions.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
</div>
|
||||
|
||||
</Show>
|
||||
|
||||
)}
|
||||
</Show>
|
||||
</Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/solid-query';
|
||||
import { fetchPendingInvitationsCount } from '../invitations.services';
|
||||
|
||||
export function usePendingInvitationsCount() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['invitations', 'count'],
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
queryFn: fetchPendingInvitationsCount,
|
||||
}));
|
||||
|
||||
return {
|
||||
...query,
|
||||
getPendingInvitationsCount: () => query.data?.pendingInvitationsCount ?? 0,
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user