mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-17 12:15:22 -06:00
Compare commits
59 Commits
@papra/app
...
@papra/web
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5382019721 | ||
|
|
b33fde35d3 | ||
|
|
fd6f83f538 | ||
|
|
7f7e5bffcb | ||
|
|
5868800bce | ||
|
|
b5ccc135ba | ||
|
|
5e46bb9e6a | ||
|
|
41a113334a | ||
|
|
6723baf98a | ||
|
|
bbe5fe74e2 | ||
|
|
a8cff8cedc | ||
|
|
67b3b14cdf | ||
|
|
ffdae8db56 | ||
|
|
7768840aa4 | ||
|
|
dd3862e50c | ||
|
|
a82ff3a755 | ||
|
|
d5b00307da | ||
|
|
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 | ||
|
|
f37c7dd8f7 | ||
|
|
a7fbf21a9f | ||
|
|
f97e5f863e | ||
|
|
8dcd6bc5ed | ||
|
|
87cb325369 | ||
|
|
e1743954d2 | ||
|
|
44b5b9fd5a | ||
|
|
68c5a3e2b7 | ||
|
|
684138c3fd | ||
|
|
0aa3241712 | ||
|
|
ad6358195e | ||
|
|
0e99669206 | ||
|
|
a91d98fb44 |
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
|
||||
@@ -84,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
|
||||
@@ -94,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
|
||||
@@ -102,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
|
||||
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @papra/docs
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#455](https://github.com/papra-hq/papra/pull/455) [`b33fde3`](https://github.com/papra-hq/papra/commit/b33fde35d3e8622e31b51aadfe56875d8e48a2ef) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Improved feedback message in case of invalid origin configuration
|
||||
|
||||
## 0.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#405](https://github.com/papra-hq/papra/pull/405) [`3401cfb`](https://github.com/papra-hq/papra/commit/3401cfbfdc7e280d2f0f3166ceddcbf55486f574) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Introduce APP_BASE_URL to mutualize server and client base url
|
||||
|
||||
- [#379](https://github.com/papra-hq/papra/pull/379) [`6cedc30`](https://github.com/papra-hq/papra/commit/6cedc30716e320946f79a0a9fd8d3b26e834f4db) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Updated dependencies
|
||||
|
||||
- [#390](https://github.com/papra-hq/papra/pull/390) [`42bc3c6`](https://github.com/papra-hq/papra/commit/42bc3c669840eb778d251dcfb0dd96b45bf6e277) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added API endpoints documentation
|
||||
|
||||
- [#402](https://github.com/papra-hq/papra/pull/402) [`1d23f40`](https://github.com/papra-hq/papra/commit/1d23f4089479387d5b87dbcf6d3819f5ee14d580) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix invalid domain in json schema urls
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@papra/docs",
|
||||
"type": "module",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.3",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.9.0",
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra documentation website",
|
||||
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
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,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[] })[] {
|
||||
@@ -45,7 +47,7 @@ const rows = configDetails
|
||||
});
|
||||
|
||||
const mdSections = rows.map(({ documentation, env, path, defaultValue }) => `
|
||||
### ${env}
|
||||
### ${env}
|
||||
${documentation}
|
||||
|
||||
- Path: \`${path.join('.')}\`
|
||||
@@ -85,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 };
|
||||
|
||||
@@ -31,6 +31,7 @@ Launch Papra with default configuration using:
|
||||
docker run -d \
|
||||
--name papra \
|
||||
--restart unless-stopped \
|
||||
--env APP_BASE_URL=http://localhost:1221 \
|
||||
-p 1221:1221 \
|
||||
ghcr.io/papra-hq/papra:latest
|
||||
```
|
||||
@@ -69,6 +70,7 @@ For production deployments, mount host directories to preserve application data
|
||||
docker run -d \
|
||||
--name papra \
|
||||
--restart unless-stopped \
|
||||
--env APP_BASE_URL=http://localhost:1221 \
|
||||
-p 1221:1221 \
|
||||
-v $(pwd)/papra-data:/app/app-data \
|
||||
--user $(id -u):$(id -g) \
|
||||
|
||||
@@ -4,14 +4,26 @@ 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.
|
||||
@@ -24,5 +24,17 @@ To fix this, you can either:
|
||||
- Ensure that the directory is owned by the user running the container
|
||||
- Run the server as root (not recommended)
|
||||
|
||||
## Invalid application origin
|
||||
|
||||
Papra ensures [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by validating the Origin header in requests. This check ensures that requests originate from the application or a trusted source. Any request that does not originate from a trusted origin will be rejected.
|
||||
|
||||
If you are self-hosting Papra, you may encounter an error stating that the application origin is invalid while trying to login or register.
|
||||
|
||||
To fix this, you can either:
|
||||
|
||||
- Update the `APP_BASE_URL` environment variable to match the url of your application (e.g. `https://papra.my-homelab.tld`)
|
||||
- Add the current url to the `TRUSTED_ORIGINS` environment variable if you need to allow multiple origins, comma separated. By default the `TRUSTED_ORIGINS` is set to the `APP_BASE_URL`
|
||||
- If you are using a reverse proxy, you may need to add the url to the `TRUSTED_ORIGINS` environment variable
|
||||
|
||||
|
||||
|
||||
|
||||
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)
|
||||
@@ -55,7 +55,10 @@ export const sidebar = [
|
||||
target: '_blank',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
label: 'API Endpoints',
|
||||
slug: 'resources/api-endpoints',
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies StarlightUserConfig['sidebar'];
|
||||
|
||||
@@ -16,8 +16,7 @@ services:
|
||||
- 1221:1221
|
||||
environment:
|
||||
- AUTH_SECRET=change-me
|
||||
- CLIENT_BASE_URL=http://localhost:1221
|
||||
- SERVER_BASE_URL=http://localhost:1221
|
||||
- APP_BASE_URL=http://localhost:1221
|
||||
volumes:
|
||||
- ./app-data:/app/app-data
|
||||
user: 1000:1000
|
||||
@@ -281,19 +280,14 @@ function getDockerComposeYml() {
|
||||
const intakeEmailEnabled = intakeEmailEnabledSelect.value === 'true';
|
||||
const intakeDriver = intakeDriverSelect.value;
|
||||
const webhookSecret = webhookSecretInput.value;
|
||||
const appBaseUrl = appBaseUrlInput.value.trim();
|
||||
const appBaseUrl = appBaseUrlInput.value.trim() || `http://localhost:${port}`;
|
||||
|
||||
const version = isRootless ? 'latest' : 'latest-root';
|
||||
const fullImage = `${image}:${version}`;
|
||||
|
||||
// Determine base URLs
|
||||
const clientBaseUrl = appBaseUrl || `http://localhost:${port}`;
|
||||
const serverBaseUrl = appBaseUrl || `http://localhost:${port}`;
|
||||
|
||||
const environment = [
|
||||
`AUTH_SECRET=${authSecret}`,
|
||||
`CLIENT_BASE_URL=${clientBaseUrl}`,
|
||||
`SERVER_BASE_URL=${serverBaseUrl}`,
|
||||
`APP_BASE_URL=${appBaseUrl}`,
|
||||
isIngestionEnabled && 'INGESTION_FOLDER_IS_ENABLED=true',
|
||||
intakeEmailEnabled && 'INTAKE_EMAILS_IS_ENABLED=true',
|
||||
intakeEmailEnabled && `INTAKE_EMAILS_DRIVER=${intakeDriver}`,
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
# @papra/app-client
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#432](https://github.com/papra-hq/papra/pull/432) [`6723baf`](https://github.com/papra-hq/papra/commit/6723baf98ad46f989fe1e1e19ad0dd25622cca77) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added new webhook events: document:updated, document:tag:added, document:tag:removed
|
||||
|
||||
- [#432](https://github.com/papra-hq/papra/pull/432) [`6723baf`](https://github.com/papra-hq/papra/commit/6723baf98ad46f989fe1e1e19ad0dd25622cca77) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Webhooks invocation is now defered
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#419](https://github.com/papra-hq/papra/pull/419) [`7768840`](https://github.com/papra-hq/papra/commit/7768840aa4425a03cb96dc1c17605bfa8e6a0de4) Thanks [@Edward205](https://github.com/Edward205)! - Added diacritics and improved wording for Romanian translation
|
||||
|
||||
- [#448](https://github.com/papra-hq/papra/pull/448) [`5868800`](https://github.com/papra-hq/papra/commit/5868800bcec6ed69b5441b50e4445fae5cdb5bfb) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added feedback when an error occurs while deleting a tag
|
||||
|
||||
- [#412](https://github.com/papra-hq/papra/pull/412) [`ffdae8d`](https://github.com/papra-hq/papra/commit/ffdae8db56c6ecfe63eb263ee606e9469eef8874) Thanks [@OsafAliSayed](https://github.com/OsafAliSayed)! - Simplified the organization intake email list
|
||||
|
||||
- [#441](https://github.com/papra-hq/papra/pull/441) [`5e46bb9`](https://github.com/papra-hq/papra/commit/5e46bb9e6a39cd16a83636018370607a27db042a) Thanks [@Zavy86](https://github.com/Zavy86)! - Added Italian (it) language support
|
||||
|
||||
- [#455](https://github.com/papra-hq/papra/pull/455) [`b33fde3`](https://github.com/papra-hq/papra/commit/b33fde35d3e8622e31b51aadfe56875d8e48a2ef) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Improved feedback message in case of invalid origin configuration
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#417](https://github.com/papra-hq/papra/pull/417) [`a82ff3a`](https://github.com/papra-hq/papra/commit/a82ff3a755fa1164b4d8ff09b591ed6482af0ccc) Thanks [@CorentinTh](https://github.com/CorentinTh)! - v0.7 release
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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,9 +1,9 @@
|
||||
{
|
||||
"name": "@papra/app-client",
|
||||
"type": "module",
|
||||
"version": "0.6.3",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.9.0",
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra frontend client",
|
||||
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -31,13 +31,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@corentinth/chisels": "^1.3.1",
|
||||
"@kobalte/core": "^0.13.9",
|
||||
"@kobalte/core": "^0.13.10",
|
||||
"@kobalte/utils": "^0.9.1",
|
||||
"@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.77.2",
|
||||
"@tanstack/solid-query": "^5.81.2",
|
||||
"@tanstack/solid-table": "^8.21.3",
|
||||
"@unocss/reset": "^0.64.1",
|
||||
"better-auth": "catalog:",
|
||||
@@ -47,7 +47,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"ofetch": "^1.4.1",
|
||||
"posthog-js": "^1.246.0",
|
||||
"posthog-js": "^1.255.1",
|
||||
"radix3": "^1.1.2",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-sonner": "^0.2.8",
|
||||
@@ -59,18 +59,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "catalog:",
|
||||
"@iconify-json/tabler": "^1.2.18",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@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.1",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"tsx": "^4.19.4",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "catalog:",
|
||||
"unocss": "0.65.0-beta.2",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-solid": "^2.11.6",
|
||||
"vite-plugin-solid": "^2.11.7",
|
||||
"vitest": "catalog:",
|
||||
"yaml": "^2.8.0"
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ auth.legal-links.description: Indem Sie fortfahren, bestätigen Sie, dass Sie di
|
||||
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
|
||||
@@ -256,6 +259,9 @@ 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.
|
||||
@@ -306,7 +312,6 @@ 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.placeholder: 'Z.B. #FF0000'
|
||||
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
|
||||
@@ -484,8 +489,12 @@ webhooks.delete.confirm.message: Sind Sie sicher, dass Sie diesen Webhook lösch
|
||||
webhooks.delete.confirm.confirm-button: Löschen
|
||||
webhooks.delete.confirm.cancel-button: Abbrechen
|
||||
|
||||
webhooks.events.documents.title: Dokumente Ereignisse
|
||||
webhooks.events.documents.document:created.description: Dokument erstellt
|
||||
webhooks.events.documents.document:deleted.description: Dokument gelöscht
|
||||
webhooks.events.documents.document:updated.description: Dokument aktualisiert
|
||||
webhooks.events.documents.document:tag:added.description: Ein Tag wurde zu einem Dokument hinzugefügt
|
||||
webhooks.events.documents.document:tag:removed.description: Ein Tag wurde von einem Dokument entfernt
|
||||
|
||||
# Navigation
|
||||
|
||||
@@ -536,6 +545,8 @@ api-errors.user.already_in_organization: Dieser Benutzer ist bereits in dieser O
|
||||
api-errors.user.organization_invitation_limit_reached: Die maximale Anzahl an Einladungen für heute wurde erreicht. Bitte versuchen Sie es morgen erneut.
|
||||
api-errors.demo.not_available: Diese Funktion ist in der Demo nicht verfügbar
|
||||
api-errors.tags.already_exists: Ein Tag mit diesem Namen existiert bereits für diese Organisation
|
||||
api-errors.internal.error: Beim Verarbeiten Ihrer Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.
|
||||
api-errors.auth.invalid_origin: Ungültige Anwendungs-Ursprung. Wenn Sie Papra selbst hosten, stellen Sie sicher, dass Ihre APP_BASE_URL-Umgebungsvariable mit Ihrer aktuellen URL übereinstimmt. Weitere Details finden Sie unter https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# Not found
|
||||
|
||||
@@ -550,3 +561,11 @@ demo.popup.discord: Treten Sie dem {{ discordLink }} bei, um Support zu erhalten
|
||||
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
|
||||
|
||||
@@ -71,6 +71,9 @@ auth.legal-links.description: By continuing, you acknowledge that you understand
|
||||
auth.legal-links.terms: Terms of Service
|
||||
auth.legal-links.privacy: Privacy Policy
|
||||
|
||||
auth.no-auth-provider.title: No authentication provider
|
||||
auth.no-auth-provider.description: There are no authentication providers enabled on this instance of Papra. Please contact the administrator of this instance to enable them.
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: User settings
|
||||
@@ -256,6 +259,9 @@ 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.
|
||||
@@ -306,7 +312,6 @@ 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.placeholder: 'Eg. #FF0000'
|
||||
tags.form.color.required: Please enter a color
|
||||
tags.form.color.invalid: The hex color is badly formatted.
|
||||
tags.form.description.label: Description
|
||||
@@ -484,8 +489,12 @@ webhooks.delete.confirm.message: Are you sure you want to delete this webhook?
|
||||
webhooks.delete.confirm.confirm-button: Delete
|
||||
webhooks.delete.confirm.cancel-button: Cancel
|
||||
|
||||
webhooks.events.documents.title: Documents events
|
||||
webhooks.events.documents.document:created.description: Document created
|
||||
webhooks.events.documents.document:deleted.description: Document deleted
|
||||
webhooks.events.documents.document:updated.description: Document updated
|
||||
webhooks.events.documents.document:tag:added.description: A tag is added to a document
|
||||
webhooks.events.documents.document:tag:removed.description: A tag is removed from a document
|
||||
|
||||
# Navigation
|
||||
|
||||
@@ -536,6 +545,8 @@ api-errors.user.already_in_organization: This user is already in this organizati
|
||||
api-errors.user.organization_invitation_limit_reached: The maximum number of invitations has been reached for today. Please try again tomorrow.
|
||||
api-errors.demo.not_available: This feature is not available in demo
|
||||
api-errors.tags.already_exists: A tag with this name already exists for this organization
|
||||
api-errors.internal.error: An error occurred while processing your request. Please try again later.
|
||||
api-errors.auth.invalid_origin: Invalid application origin. If you are self-hosting Papra, ensure your APP_BASE_URL environment variable matches your current url. For more details see https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# Not found
|
||||
|
||||
@@ -550,3 +561,11 @@ demo.popup.discord: Join the {{ discordLink }} to get support, propose features
|
||||
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
|
||||
|
||||
571
apps/papra-client/src/locales/es.yml
Normal file
571
apps/papra-client/src/locales/es.yml
Normal file
@@ -0,0 +1,571 @@
|
||||
# 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.title: Eventos de documentos
|
||||
webhooks.events.documents.document:created.description: Documento creado
|
||||
webhooks.events.documents.document:deleted.description: Documento eliminado
|
||||
webhooks.events.documents.document:updated.description: Documento actualizado
|
||||
webhooks.events.documents.document:tag:added.description: Una etiqueta se ha añadido a un documento
|
||||
webhooks.events.documents.document:tag:removed.description: Una etiqueta se ha eliminado de un documento
|
||||
|
||||
# Navigation
|
||||
|
||||
layout.menu.home: Inicio
|
||||
layout.menu.documents: Documentos
|
||||
layout.menu.tags: Etiquetas
|
||||
layout.menu.tagging-rules: Reglas de etiquetado
|
||||
layout.menu.deleted-documents: Documentos eliminados
|
||||
layout.menu.organization-settings: Configuración
|
||||
layout.menu.api-keys: Claves API
|
||||
layout.menu.settings: Ajustes
|
||||
layout.menu.account: Cuenta
|
||||
layout.menu.general-settings: Ajustes generales
|
||||
layout.menu.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
|
||||
api-errors.internal.error: Ocurrió un error al procesar tu solicitud. Por favor, inténtalo de nuevo.
|
||||
api-errors.auth.invalid_origin: Origen de la aplicación inválido. Si estás alojando Papra, asegúrate de que la variable de entorno APP_BASE_URL coincida con tu URL actual. Para más detalles, consulta https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# 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
|
||||
@@ -71,6 +71,9 @@ auth.legal-links.description: En continuant, vous reconnaissez que vous comprene
|
||||
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
|
||||
@@ -256,6 +259,9 @@ 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.
|
||||
@@ -306,7 +312,6 @@ 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.placeholder: 'Exemple: #FF0000'
|
||||
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
|
||||
@@ -484,8 +489,12 @@ webhooks.delete.confirm.message: Êtes-vous sûr de vouloir supprimer ce webhook
|
||||
webhooks.delete.confirm.confirm-button: Supprimer
|
||||
webhooks.delete.confirm.cancel-button: Annuler
|
||||
|
||||
webhooks.events.documents.title: Événements de documents
|
||||
webhooks.events.documents.document:created.description: Document créé
|
||||
webhooks.events.documents.document:deleted.description: Document supprimé
|
||||
webhooks.events.documents.document:updated.description: Document mis à jour
|
||||
webhooks.events.documents.document:tag:added.description: Un tag est ajouté à un document
|
||||
webhooks.events.documents.document:tag:removed.description: Un tag est retiré d'un document
|
||||
|
||||
# Navigation
|
||||
|
||||
@@ -536,6 +545,8 @@ api-errors.user.already_in_organization: Cet utilisateur est déjà dans cette o
|
||||
api-errors.user.organization_invitation_limit_reached: Le nombre maximum d'invitations a été atteint pour aujourd'hui. Veuillez réessayer demain.
|
||||
api-errors.demo.not_available: Cette fonctionnalité n'est pas disponible dans la démo
|
||||
api-errors.tags.already_exists: Un tag avec ce nom existe déjà pour cette organisation
|
||||
api-errors.internal.error: Une erreur est survenue lors du traitement de votre requête. Veuillez réessayer.
|
||||
api-errors.auth.invalid_origin: Origine de l'application invalide. Si vous hébergez Papra, assurez-vous que la variable d'environnement APP_BASE_URL correspond à votre URL actuelle. Pour plus de détails, consultez https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# Not found
|
||||
|
||||
@@ -550,3 +561,11 @@ demo.popup.discord: Rejoignez le {{ discordLink }} pour obtenir de l'aide, propo
|
||||
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
|
||||
|
||||
571
apps/papra-client/src/locales/it.yml
Normal file
571
apps/papra-client/src/locales/it.yml
Normal file
@@ -0,0 +1,571 @@
|
||||
# Authentication
|
||||
|
||||
auth.request-password-reset.title: Reimposta la tua password
|
||||
auth.request-password-reset.description: Inserisci la tua email per reimpostare la password.
|
||||
auth.request-password-reset.requested: Se esiste un account per questa email, ti abbiamo inviato un'email per reimpostare la password.
|
||||
auth.request-password-reset.back-to-login: Torna al login
|
||||
auth.request-password-reset.form.email.label: Email
|
||||
auth.request-password-reset.form.email.placeholder: 'Esempio: ada@papra.app'
|
||||
auth.request-password-reset.form.email.required: Inserisci il tuo indirizzo email
|
||||
auth.request-password-reset.form.email.invalid: Questo indirizzo email non è valido
|
||||
auth.request-password-reset.form.submit: Richiedi reimpostazione password
|
||||
|
||||
auth.reset-password.title: Reimposta la tua password
|
||||
auth.reset-password.description: Inserisci la nuova password per reimpostare la password.
|
||||
auth.reset-password.reset: La tua password è stata reimpostata.
|
||||
auth.reset-password.back-to-login: Torna al login
|
||||
auth.reset-password.form.new-password.label: Nuova password
|
||||
auth.reset-password.form.new-password.placeholder: 'Esempio: **********'
|
||||
auth.reset-password.form.new-password.required: Inserisci la tua nuova password
|
||||
auth.reset-password.form.new-password.min-length: La password deve essere di almeno {{ minLength }} caratteri
|
||||
auth.reset-password.form.new-password.max-length: La password deve essere inferiore a {{ maxLength }} caratteri
|
||||
auth.reset-password.form.submit: Reimposta password
|
||||
|
||||
auth.email-provider.open: Apri {{ provider }}
|
||||
|
||||
auth.login.title: Accedi a Papra
|
||||
auth.login.description: Inserisci la tua email o usa un provider per accedere al tuo account Papra.
|
||||
auth.login.login-with-provider: Accedi con {{ provider }}
|
||||
auth.login.no-account: Non hai un account?
|
||||
auth.login.register: Registrati
|
||||
auth.login.form.email.label: Email
|
||||
auth.login.form.email.placeholder: 'Esempio: ada@papra.app'
|
||||
auth.login.form.email.required: Inserisci il tuo indirizzo email
|
||||
auth.login.form.email.invalid: Questo indirizzo email non è valido
|
||||
auth.login.form.password.label: Password
|
||||
auth.login.form.password.placeholder: Imposta una password
|
||||
auth.login.form.password.required: Inserisci la tua password
|
||||
auth.login.form.remember-me.label: Ricordami
|
||||
auth.login.form.forgot-password.label: Password dimenticata?
|
||||
auth.login.form.submit: Accedi
|
||||
|
||||
auth.register.title: Registrati a Papra
|
||||
auth.register.description: Crea un account per iniziare a usare Papra.
|
||||
auth.register.register-with-email: Registrati tramite email
|
||||
auth.register.register-with-provider: Registrati tramite {{ provider }}
|
||||
auth.register.providers.google: Google
|
||||
auth.register.providers.github: GitHub
|
||||
auth.register.have-account: Hai già un account?
|
||||
auth.register.login: Accedi
|
||||
auth.register.registration-disabled.title: Registrazione disabilitata
|
||||
auth.register.registration-disabled.description: La creazione di nuovi account è attualmente disabilitata su questa istanza di Papra. Solo gli utenti con account esistenti possono accedere. Se pensi che sia un errore, contatta l'amministratore di questa istanza.
|
||||
auth.register.form.email.label: Email
|
||||
auth.register.form.email.placeholder: 'Esempio: ada@papra.app'
|
||||
auth.register.form.email.required: Inserisci il tuo indirizzo email
|
||||
auth.register.form.email.invalid: Questo indirizzo email non è valido
|
||||
auth.register.form.password.label: Password
|
||||
auth.register.form.password.placeholder: Imposta una password
|
||||
auth.register.form.password.required: Inserisci la tua password
|
||||
auth.register.form.password.min-length: La password deve essere di almeno {{ minLength }} caratteri
|
||||
auth.register.form.password.max-length: La password deve essere inferiore a {{ maxLength }} caratteri
|
||||
auth.register.form.name.label: Nome
|
||||
auth.register.form.name.placeholder: 'Esempio: Ada Lovelace'
|
||||
auth.register.form.name.required: Inserisci il tuo nome
|
||||
auth.register.form.name.max-length: Il nome deve essere inferiore a {{ maxLength }} caratteri
|
||||
auth.register.form.submit: Registrati
|
||||
|
||||
auth.email-validation-required.title: Verifica la tua email
|
||||
auth.email-validation-required.description: Una email di verifica è stata inviata al tuo indirizzo email. Verifica il tuo indirizzo cliccando il link nell'email.
|
||||
|
||||
auth.legal-links.description: Continuando, confermi di aver letto e accettato i {{ terms }} e l'{{ privacy }}.
|
||||
auth.legal-links.terms: Termini di servizio
|
||||
auth.legal-links.privacy: Informativa sulla privacy
|
||||
|
||||
auth.no-auth-provider.title: Nessun provider di autenticazione
|
||||
auth.no-auth-provider.description: Nessun provider di autenticazione è abilitato su questa istanza di Papra. Contatta l'amministratore di questa istanza per abilitarli.
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: Impostazioni utente
|
||||
user.settings.description: Gestisci qui le impostazioni del tuo account.
|
||||
|
||||
user.settings.email.title: Indirizzo email
|
||||
user.settings.email.description: Il tuo indirizzo email non può essere modificato.
|
||||
user.settings.email.label: Indirizzo email
|
||||
|
||||
user.settings.name.title: Nome completo
|
||||
user.settings.name.description: Il tuo nome completo è visibile agli altri membri dell'organizzazione.
|
||||
user.settings.name.label: Nome completo
|
||||
user.settings.name.placeholder: Es. Mario Rossi
|
||||
user.settings.name.update: Aggiorna nome
|
||||
user.settings.name.updated: Il tuo nome completo è stato aggiornato
|
||||
|
||||
user.settings.logout.title: Logout
|
||||
user.settings.logout.description: Esci dal tuo account. Potrai accedere nuovamente in seguito.
|
||||
user.settings.logout.button: Esci
|
||||
|
||||
# Organizations
|
||||
|
||||
organizations.list.title: Le tue organizzazioni
|
||||
organizations.list.description: Le organizzazioni sono un modo per raggruppare i tuoi documenti e gestire l'accesso. Puoi creare più organizzazioni e invitare i tuoi collaboratori.
|
||||
organizations.list.create-new: Crea una nuova organizzazione
|
||||
|
||||
organizations.details.no-documents.title: Nessun documento
|
||||
organizations.details.no-documents.description: Non ci sono ancora documenti in questa organizzazione. Inizia caricando dei documenti.
|
||||
organizations.details.upload-documents: Carica documenti
|
||||
organizations.details.documents-count: documenti in totale
|
||||
organizations.details.total-size: dimensione totale
|
||||
organizations.details.latest-documents: Ultimi documenti importati
|
||||
|
||||
organizations.create.title: Crea una nuova organizzazione
|
||||
organizations.create.description: I tuoi documenti saranno raggruppati per organizzazione. Puoi creare più organizzazioni per separare i documenti, ad esempio per uso personale e lavorativo.
|
||||
organizations.create.back: Indietro
|
||||
organizations.create.error.max-count-reached: Hai raggiunto il numero massimo di organizzazioni che puoi creare, se hai bisogno di crearne altre contatta il supporto.
|
||||
organizations.create.form.name.label: Nome organizzazione
|
||||
organizations.create.form.name.placeholder: Es. Acme Inc.
|
||||
organizations.create.form.name.required: Inserisci il nome dell'organizzazione
|
||||
organizations.create.form.submit: Crea organizzazione
|
||||
organizations.create.success: Organizzazione creata con successo
|
||||
|
||||
organizations.create-first.title: Crea la tua organizzazione
|
||||
organizations.create-first.description: I tuoi documenti saranno raggruppati per organizzazione. Puoi creare più organizzazioni per separare i documenti, ad esempio per uso personale e lavorativo.
|
||||
organizations.create-first.default-name: La mia organizzazione
|
||||
organizations.create-first.user-name: 'Organizzazione di {{ name }}'
|
||||
|
||||
organization.settings.title: Impostazioni organizzazione
|
||||
organization.settings.page.title: Impostazioni organizzazione
|
||||
organization.settings.page.description: Gestisci qui le impostazioni della tua organizzazione.
|
||||
organization.settings.name.title: Nome organizzazione
|
||||
organization.settings.name.update: Aggiorna nome
|
||||
organization.settings.name.placeholder: Es. Acme Inc.
|
||||
organization.settings.name.updated: Nome organizzazione aggiornato
|
||||
organization.settings.subscription.title: Sottoscrizione
|
||||
organization.settings.subscription.description: Gestisci fatturazione, fatture e metodi di pagamento.
|
||||
organization.settings.subscription.manage: Gestisci sottoscrizione
|
||||
organization.settings.subscription.error: Impossibile ottenere l'URL del portale clienti
|
||||
organization.settings.delete.title: Elimina organizzazione
|
||||
organization.settings.delete.description: Eliminando questa organizzazione rimuoverai definitivamente tutti i dati associati.
|
||||
organization.settings.delete.confirm.title: Elimina organizzazione
|
||||
organization.settings.delete.confirm.message: Sei sicuro di voler eliminare questa organizzazione? Questa azione non può essere annullata e tutti i dati associati saranno rimossi in modo permanente.
|
||||
organization.settings.delete.confirm.confirm-button: Elimina organizzazione
|
||||
organization.settings.delete.confirm.cancel-button: Annulla
|
||||
organization.settings.delete.success: Organizzazione eliminata
|
||||
|
||||
organizations.members.title: Membri
|
||||
organizations.members.description: Gestisci i membri della tua organizzazione
|
||||
organizations.members.invite-member: Invita membro
|
||||
organizations.members.invite-member-disabled-tooltip: Solo gli amministratori o i proprietari possono invitare membri nell'organizzazione
|
||||
organizations.members.remove-from-organization: Rimuovi dall'organizzazione
|
||||
organizations.members.role: Ruolo
|
||||
organizations.members.roles.owner: Proprietario
|
||||
organizations.members.roles.admin: Amministratore
|
||||
organizations.members.roles.member: Membro
|
||||
organizations.members.delete.confirm.title: Rimuovi membro
|
||||
organizations.members.delete.confirm.message: Sei sicuro di voler rimuovere questo membro dall'organizzazione?
|
||||
organizations.members.delete.confirm.confirm-button: Rimuovi
|
||||
organizations.members.delete.confirm.cancel-button: Annulla
|
||||
organizations.members.delete.success: Membro rimosso dall'organizzazione
|
||||
organizations.members.update-role.success: Ruolo del membro aggiornato
|
||||
organizations.members.table.headers.name: Nome
|
||||
organizations.members.table.headers.email: Email
|
||||
organizations.members.table.headers.role: Ruolo
|
||||
organizations.members.table.headers.created: Creato
|
||||
organizations.members.table.headers.actions: Azioni
|
||||
|
||||
organizations.invite-member.title: Invita membro
|
||||
organizations.invite-member.description: Invita un membro nella tua organizzazione
|
||||
organizations.invite-member.form.email.label: Email
|
||||
organizations.invite-member.form.email.placeholder: 'Esempio: ada@papra.app'
|
||||
organizations.invite-member.form.email.required: Inserisci un indirizzo email valido
|
||||
organizations.invite-member.form.role.label: Ruolo
|
||||
organizations.invite-member.form.submit: Invita nell'organizzazione
|
||||
organizations.invite-member.success.message: Membro invitato
|
||||
organizations.invite-member.success.description: Il membro è stato invitato nell'organizzazione.
|
||||
organizations.invite-member.error.message: Impossibile invitare il membro
|
||||
|
||||
organizations.invitations.title: Inviti
|
||||
organizations.invitations.description: Gestisci gli inviti della tua organizzazione
|
||||
organizations.invitations.list.cta: Invita membro
|
||||
organizations.invitations.list.empty.title: Nessun invito in sospeso
|
||||
organizations.invitations.list.empty.description: Non sei stato ancora invitato in nessuna organizzazione.
|
||||
organizations.invitations.status.pending: In sospeso
|
||||
organizations.invitations.status.accepted: Accettato
|
||||
organizations.invitations.status.rejected: Rifiutato
|
||||
organizations.invitations.status.expired: Scaduto
|
||||
organizations.invitations.status.cancelled: Cancellato
|
||||
organizations.invitations.resend: Invia di nuovo invito
|
||||
organizations.invitations.cancel.title: Annulla invito
|
||||
organizations.invitations.cancel.description: Sei sicuro di voler annullare questo invito?
|
||||
organizations.invitations.cancel.confirm: Annulla invito
|
||||
organizations.invitations.cancel.cancel: Annulla
|
||||
organizations.invitations.resend.title: Invia di nuovo invito
|
||||
organizations.invitations.resend.description: Sei sicuro di voler inviare nuovamente questo invito? Sarà inviata una nuova email al destinatario.
|
||||
organizations.invitations.resend.confirm: Invia invito
|
||||
organizations.invitations.resend.cancel: Annulla
|
||||
|
||||
invitations.list.title: Inviti
|
||||
invitations.list.description: Gestisci gli inviti della tua organizzazione
|
||||
invitations.list.empty.title: Nessun invito in sospeso
|
||||
invitations.list.empty.description: Non sei stato ancora invitato in nessuna organizzazione.
|
||||
invitations.list.headers.organization: Organizzazione
|
||||
invitations.list.headers.status: Stato
|
||||
invitations.list.headers.created: Creato
|
||||
invitations.list.headers.actions: Azioni
|
||||
invitations.list.actions.accept: Accetta
|
||||
invitations.list.actions.reject: Rifiuta
|
||||
invitations.list.actions.accept.success.message: Invito accettato
|
||||
invitations.list.actions.accept.success.description: L'invito è stato accettato.
|
||||
invitations.list.actions.reject.success.message: Invito rifiutato
|
||||
invitations.list.actions.reject.success.description: L'invito è stato rifiutato.
|
||||
|
||||
# Documents
|
||||
|
||||
documents.list.title: Documenti
|
||||
documents.list.no-documents.title: Nessun documento
|
||||
documents.list.no-documents.description: Non ci sono ancora documenti in questa organizzazione. Inizia caricando dei documenti.
|
||||
documents.list.no-results: Nessun documento trovato
|
||||
|
||||
documents.tabs.info: Info
|
||||
documents.tabs.content: Contenuto
|
||||
documents.tabs.activity: Attività
|
||||
documents.deleted.message: Questo documento è stato eliminato e sarà rimosso definitivamente tra {{ days }} giorni.
|
||||
documents.actions.download: Scarica
|
||||
documents.actions.open-in-new-tab: Apri in una nuova scheda
|
||||
documents.actions.restore: Ripristina
|
||||
documents.actions.delete: Elimina
|
||||
documents.actions.edit: Modifica
|
||||
documents.actions.cancel: Annulla
|
||||
documents.actions.save: Salva
|
||||
documents.actions.saving: Salvataggio in corso...
|
||||
documents.content.alert: Il contenuto del documento è estratto automaticamente al caricamento. È usato solo per la ricerca e l'indicizzazione.
|
||||
documents.info.id: ID
|
||||
documents.info.name: Nome
|
||||
documents.info.type: Tipo
|
||||
documents.info.size: Dimensione
|
||||
documents.info.created-at: Creato il
|
||||
documents.info.updated-at: Aggiornato il
|
||||
documents.info.never: Mai
|
||||
|
||||
documents.rename.title: Rinomina documento
|
||||
documents.rename.form.name.label: Nome
|
||||
documents.rename.form.name.placeholder: 'Esempio: Fattura 2024'
|
||||
documents.rename.form.name.required: Inserisci un nome per il documento
|
||||
documents.rename.form.name.max-length: Il nome deve essere inferiore a 255 caratteri
|
||||
documents.rename.form.submit: Rinomina documento
|
||||
documents.rename.success: Documento rinominato con successo
|
||||
documents.rename.cancel: Annulla
|
||||
|
||||
import-documents.title.error: '{{ count }} documenti non importati'
|
||||
import-documents.title.success: '{{ count }} documenti importati'
|
||||
import-documents.title.pending: '{{ count }} / {{ total }} documenti importati'
|
||||
import-documents.title.none: Importa documenti
|
||||
import-documents.no-import-in-progress: Nessuna importazione documenti in corso
|
||||
|
||||
documents.deleted.title: Documenti eliminati
|
||||
documents.deleted.empty.title: Nessun documento eliminato
|
||||
documents.deleted.empty.description: Non hai documenti eliminati. I documenti eliminati saranno spostati nel cestino per {{ days }} giorni.
|
||||
documents.deleted.retention-notice: Tutti i documenti eliminati sono conservati nel cestino per {{ days }} giorni. Passato questo periodo, saranno eliminati definitivamente e non potrai recuperarli.
|
||||
documents.deleted.deleted-at: Eliminato il
|
||||
documents.deleted.restoring: Ripristino in corso...
|
||||
documents.deleted.deleting: Eliminazione in corso...
|
||||
|
||||
documents.preview.unknown-file-type: Nessuna anteprima disponibile per questo tipo di file
|
||||
documents.preview.binary-file: Sembra essere un file binario e non può essere visualizzato come testo
|
||||
|
||||
trash.delete-all.button: Elimina tutto
|
||||
trash.delete-all.confirm.title: Eliminare definitivamente tutti i documenti?
|
||||
trash.delete-all.confirm.description: Sei sicuro di voler eliminare definitivamente tutti i documenti dal cestino? Questa azione non può essere annullata.
|
||||
trash.delete-all.confirm.label: Elimina
|
||||
trash.delete-all.confirm.cancel: Annulla
|
||||
trash.delete.button: Elimina
|
||||
trash.delete.confirm.title: Eliminare definitivamente il documento?
|
||||
trash.delete.confirm.description: Sei sicuro di voler eliminare definitivamente questo documento dal cestino? Questa azione non può essere annullata.
|
||||
trash.delete.confirm.label: Elimina
|
||||
trash.delete.confirm.cancel: Annulla
|
||||
trash.deleted.success.title: Documento eliminato
|
||||
trash.deleted.success.description: Il documento è stato eliminato definitivamente.
|
||||
|
||||
activity.document.created: Documento creato
|
||||
activity.document.updated.single: Il campo {{ field }} è stato aggiornato
|
||||
activity.document.updated.multiple: I campi {{ fields }} sono stati aggiornati
|
||||
activity.document.updated: Documento aggiornato
|
||||
activity.document.deleted: Documento eliminato
|
||||
activity.document.restored: Documento ripristinato
|
||||
activity.document.tagged: Tag {{ tag }} aggiunto
|
||||
activity.document.untagged: Tag {{ tag }} rimosso
|
||||
|
||||
activity.document.user.name: da {{ name }}
|
||||
|
||||
activity.load-more: Carica altri
|
||||
activity.no-more-activities: Nessuna altra attività per questo documento
|
||||
|
||||
# Tags
|
||||
|
||||
tags.no-tags.title: Nessun tag
|
||||
tags.no-tags.description: Questa organizzazione non ha ancora tag. I tag vengono usati per categorizzare i documenti. Puoi aggiungere tag ai tuoi documenti per trovarli e organizzarli più facilmente.
|
||||
tags.no-tags.create-tag: Crea tag
|
||||
|
||||
tags.title: Tag dei documenti
|
||||
tags.description: I tag vengono usati per categorizzare i documenti. Puoi aggiungere tag ai tuoi documenti per trovarli e organizzarli più facilmente.
|
||||
tags.create: Crea tag
|
||||
tags.update: Aggiorna tag
|
||||
tags.delete: Elimina tag
|
||||
tags.delete.confirm.title: Elimina tag
|
||||
tags.delete.confirm.message: Sei sicuro di voler eliminare questo tag? Il tag verrà rimosso da tutti i documenti.
|
||||
tags.delete.confirm.confirm-button: Elimina
|
||||
tags.delete.confirm.cancel-button: Annulla
|
||||
tags.delete.success: Tag eliminato con successo
|
||||
tags.create.success: Tag "{{ name }}" creato con successo.
|
||||
tags.update.success: Tag "{{ name }}" aggiornato con successo.
|
||||
tags.form.name.label: Nome
|
||||
tags.form.name.placeholder: Es. Contratti
|
||||
tags.form.name.required: Inserisci un nome per il tag
|
||||
tags.form.name.max-length: Il nome del tag deve essere inferiore a 64 caratteri
|
||||
tags.form.color.label: Colore
|
||||
tags.form.color.required: Inserisci un colore
|
||||
tags.form.color.invalid: Il colore hex non è formattato correttamente.
|
||||
tags.form.description.label: Descrizione
|
||||
tags.form.description.optional: (opzionale)
|
||||
tags.form.description.placeholder: Es. Tutti i contratti firmati dall'azienda
|
||||
tags.form.description.max-length: La descrizione deve essere inferiore a 256 caratteri
|
||||
tags.form.no-description: Nessuna descrizione
|
||||
tags.table.headers.tag: Tag
|
||||
tags.table.headers.description: Descrizione
|
||||
tags.table.headers.documents: Documenti
|
||||
tags.table.headers.created: Creato
|
||||
tags.table.headers.actions: Azioni
|
||||
|
||||
# Tagging rules
|
||||
|
||||
tagging-rules.field.name: nome documento
|
||||
tagging-rules.field.content: contenuto documento
|
||||
tagging-rules.operator.equals: uguale a
|
||||
tagging-rules.operator.not-equals: diverso da
|
||||
tagging-rules.operator.contains: contiene
|
||||
tagging-rules.operator.not-contains: non contiene
|
||||
tagging-rules.operator.starts-with: inizia con
|
||||
tagging-rules.operator.ends-with: termina con
|
||||
tagging-rules.list.title: Regole di tagging
|
||||
tagging-rules.list.description: Gestisci le regole di tagging della tua organizzazione per taggare automaticamente i documenti in base a condizioni definite da te.
|
||||
tagging-rules.list.demo-warning: 'Nota: Essendo un ambiente demo (senza server), le regole di tagging non verranno applicate ai nuovi documenti.'
|
||||
tagging-rules.list.no-tagging-rules.title: Nessuna regola di tagging
|
||||
tagging-rules.list.no-tagging-rules.description: Crea una regola per taggare automaticamente i documenti aggiunti in base a condizioni definite da te.
|
||||
tagging-rules.list.no-tagging-rules.create-tagging-rule: Crea regola di tagging
|
||||
tagging-rules.list.card.no-conditions: Nessuna condizione
|
||||
tagging-rules.list.card.one-condition: 1 condizione
|
||||
tagging-rules.list.card.conditions: '{{ count }} condizioni'
|
||||
tagging-rules.list.card.delete: Elimina regola
|
||||
tagging-rules.list.card.edit: Modifica regola
|
||||
tagging-rules.create.title: Crea regola di tagging
|
||||
tagging-rules.create.success: Regola di tagging creata con successo
|
||||
tagging-rules.create.error: Errore nella creazione della regola di tagging
|
||||
tagging-rules.create.submit: Crea regola
|
||||
tagging-rules.form.name.label: Nome
|
||||
tagging-rules.form.name.placeholder: 'Esempio: Tagga fatture'
|
||||
tagging-rules.form.name.min-length: Inserisci un nome per la regola
|
||||
tagging-rules.form.name.max-length: Il nome deve essere inferiore a 64 caratteri
|
||||
tagging-rules.form.description.label: Descrizione
|
||||
tagging-rules.form.description.placeholder: "Esempio: Tagga i documenti con 'fattura' nel nome"
|
||||
tagging-rules.form.description.max-length: La descrizione deve essere inferiore a 256 caratteri
|
||||
tagging-rules.form.conditions.label: Condizioni
|
||||
tagging-rules.form.conditions.description: Definisci le condizioni che devono essere soddisfatte affinché la regola si applichi. Tutte le condizioni devono essere soddisfatte.
|
||||
tagging-rules.form.conditions.add-condition: Aggiungi condizione
|
||||
tagging-rules.form.conditions.no-conditions.title: Nessuna condizione
|
||||
tagging-rules.form.conditions.no-conditions.description: Non hai aggiunto nessuna condizione a questa regola. Questa regola applicherà i suoi tag a tutti i documenti.
|
||||
tagging-rules.form.conditions.no-conditions.confirm: Applica regola senza condizioni
|
||||
tagging-rules.form.conditions.no-conditions.cancel: Annulla
|
||||
tagging-rules.form.conditions.value.placeholder: 'Esempio: fattura'
|
||||
tagging-rules.form.conditions.value.min-length: Inserisci un valore per la condizione
|
||||
tagging-rules.form.tags.label: Tag
|
||||
tagging-rules.form.tags.description: Seleziona i tag da applicare ai documenti che soddisfano le condizioni
|
||||
tagging-rules.form.tags.min-length: È richiesto almeno un tag da applicare
|
||||
tagging-rules.form.tags.add-tag: Crea tag
|
||||
tagging-rules.form.submit: Crea regola
|
||||
tagging-rules.update.title: Aggiorna regola di tagging
|
||||
tagging-rules.update.error: Errore nell'aggiornamento della regola di tagging
|
||||
tagging-rules.update.submit: Aggiorna regola
|
||||
tagging-rules.update.cancel: Annulla
|
||||
|
||||
# Intake emails
|
||||
|
||||
intake-emails.title: Email di acquisizione
|
||||
intake-emails.description: Gli indirizzi email di acquisizione vengono usati per importare automaticamente email in Papra. Basta inoltrare le email all'indirizzo di acquisizione e gli allegati saranno aggiunti ai documenti dell'organizzazione.
|
||||
intake-emails.disabled.title: Email di acquisizione disabilitate
|
||||
intake-emails.disabled.description: Le email di acquisizione sono disabilitate su questa istanza. Contatta il tuo amministratore per abilitarle. Consulta la {{ documentation }} per maggiori informazioni.
|
||||
intake-emails.disabled.documentation: documentazione
|
||||
intake-emails.info: Solo le email di acquisizione abilitate provenienti da origini consentite saranno processate. Puoi abilitare o disabilitare un'email di acquisizione in qualsiasi momento.
|
||||
intake-emails.empty.title: Nessuna email di acquisizione
|
||||
intake-emails.empty.description: Genera un indirizzo di acquisizione per importare facilmente allegati email.
|
||||
intake-emails.empty.generate: Genera email di acquisizione
|
||||
intake-emails.count: '{{ count }} email di acquisizione per questa organizzazione'
|
||||
intake-emails.new: Nuova email di acquisizione
|
||||
intake-emails.disabled-label: (Disabilitata)
|
||||
intake-emails.no-origins: Nessuna origine email consentita
|
||||
intake-emails.allowed-origins: Consentito da {{ count }} indirizzo/i
|
||||
intake-emails.actions.enable: Abilita
|
||||
intake-emails.actions.disable: Disabilita
|
||||
intake-emails.actions.manage-origins: Gestisci indirizzi origine
|
||||
intake-emails.actions.delete: Elimina
|
||||
intake-emails.delete.confirm.title: Eliminare l'email di acquisizione?
|
||||
intake-emails.delete.confirm.message: Sei sicuro di voler eliminare questa email di acquisizione? Questa azione non può essere annullata.
|
||||
intake-emails.delete.confirm.confirm-button: Elimina email di acquisizione
|
||||
intake-emails.delete.confirm.cancel-button: Annulla
|
||||
intake-emails.delete.success: Email di acquisizione eliminata
|
||||
intake-emails.create.success: Email di acquisizione creata
|
||||
intake-emails.update.success.enabled: Email di acquisizione abilitata
|
||||
intake-emails.update.success.disabled: Email di acquisizione disabilitata
|
||||
intake-emails.allowed-origins.title: Origini consentite
|
||||
intake-emails.allowed-origins.description: Solo le email inviate a {{ email }} da queste origini saranno processate. Se non sono specificate origini, tutte le email saranno scartate.
|
||||
intake-emails.allowed-origins.add.label: Aggiungi email origine consentita
|
||||
intake-emails.allowed-origins.add.placeholder: Es. ada@papra.app
|
||||
intake-emails.allowed-origins.add.button: Aggiungi
|
||||
intake-emails.allowed-origins.add.error.exists: Questa email è già tra le origini consentite per questa email di acquisizione
|
||||
|
||||
# API keys
|
||||
|
||||
api-keys.permissions.documents.title: Documenti
|
||||
api-keys.permissions.documents.documents:create: Crea documenti
|
||||
api-keys.permissions.documents.documents:read: Leggi documenti
|
||||
api-keys.permissions.documents.documents:update: Aggiorna documenti
|
||||
api-keys.permissions.documents.documents:delete: Elimina documenti
|
||||
api-keys.permissions.tags.title: Tag
|
||||
api-keys.permissions.tags.tags:create: Crea tag
|
||||
api-keys.permissions.tags.tags:read: Leggi tag
|
||||
api-keys.permissions.tags.tags:update: Aggiorna tag
|
||||
api-keys.permissions.tags.tags:delete: Elimina tag
|
||||
api-keys.create.title: Crea chiave API
|
||||
api-keys.create.description: Crea una nuova chiave API per accedere all'API di Papra.
|
||||
api-keys.create.success: La chiave API è stata creata con successo.
|
||||
api-keys.create.back: Torna alle chiavi API
|
||||
api-keys.create.form.name.label: Nome
|
||||
api-keys.create.form.name.placeholder: 'Esempio: La mia chiave API'
|
||||
api-keys.create.form.name.required: Inserisci un nome per la chiave API
|
||||
api-keys.create.form.permissions.label: Permessi
|
||||
api-keys.create.form.permissions.required: Seleziona almeno un permesso
|
||||
api-keys.create.form.submit: Crea chiave API
|
||||
api-keys.create.created.title: Chiave API creata
|
||||
api-keys.create.created.description: La chiave API è stata creata con successo. Salvala in un luogo sicuro, non verrà più mostrata.
|
||||
api-keys.list.title: Chiavi API
|
||||
api-keys.list.description: Gestisci qui le tue chiavi API.
|
||||
api-keys.list.create: Crea chiave API
|
||||
api-keys.list.empty.title: Nessuna chiave API
|
||||
api-keys.list.empty.description: Crea una chiave API per accedere all'API di Papra.
|
||||
api-keys.list.card.last-used: Ultimo utilizzo
|
||||
api-keys.list.card.never: Mai
|
||||
api-keys.list.card.created: Creato
|
||||
api-keys.delete.success: La chiave API è stata eliminata con successo
|
||||
api-keys.delete.confirm.title: Eliminare la chiave API
|
||||
api-keys.delete.confirm.message: Sei sicuro di voler eliminare questa chiave API? Questa azione non può essere annullata.
|
||||
api-keys.delete.confirm.confirm-button: Elimina
|
||||
api-keys.delete.confirm.cancel-button: Annulla
|
||||
|
||||
# Webhooks
|
||||
|
||||
webhooks.list.title: Webhook
|
||||
webhooks.list.description: Gestisci i webhook della tua organizzazione
|
||||
webhooks.list.empty.title: Nessun webhook
|
||||
webhooks.list.empty.description: Crea il tuo primo webhook per iniziare a ricevere eventi
|
||||
webhooks.list.create: Crea webhook
|
||||
webhooks.list.card.last-triggered: Ultima attivazione
|
||||
webhooks.list.card.never: Mai
|
||||
webhooks.list.card.created: Creato
|
||||
webhooks.create.title: Crea webhook
|
||||
webhooks.create.description: Crea un nuovo webhook per ricevere eventi
|
||||
webhooks.create.success: Webhook creato con successo
|
||||
webhooks.create.back: Indietro
|
||||
webhooks.create.form.submit: Crea webhook
|
||||
webhooks.create.form.name.label: Nome webhook
|
||||
webhooks.create.form.name.placeholder: Inserisci nome webhook
|
||||
webhooks.create.form.name.required: Il nome è obbligatorio
|
||||
webhooks.create.form.url.label: URL webhook
|
||||
webhooks.create.form.url.placeholder: Inserisci URL webhook
|
||||
webhooks.create.form.url.required: L'URL è obbligatorio
|
||||
webhooks.create.form.url.invalid: L'URL non è valido
|
||||
webhooks.create.form.secret.label: Segreto
|
||||
webhooks.create.form.secret.placeholder: Inserisci il segreto del webhook
|
||||
webhooks.create.form.events.label: Eventi
|
||||
webhooks.create.form.events.required: È richiesto almeno un evento
|
||||
webhooks.update.title: Modifica webhook
|
||||
webhooks.update.description: Aggiorna i dettagli del webhook
|
||||
webhooks.update.success: Webhook aggiornato con successo
|
||||
webhooks.update.submit: Aggiorna webhook
|
||||
webhooks.update.cancel: Annulla
|
||||
webhooks.update.form.secret.placeholder: Inserisci nuovo segreto
|
||||
webhooks.update.form.secret.placeholder-redacted: '[Segreto nascosto]'
|
||||
webhooks.update.form.rotate-secret.button: Rigenera segreto
|
||||
webhooks.delete.success: Webhook eliminato con successo
|
||||
webhooks.delete.confirm.title: Eliminare webhook
|
||||
webhooks.delete.confirm.message: Sei sicuro di voler eliminare questo webhook?
|
||||
webhooks.delete.confirm.confirm-button: Elimina
|
||||
webhooks.delete.confirm.cancel-button: Annulla
|
||||
|
||||
webhooks.events.documents.title: Eventi documenti
|
||||
webhooks.events.documents.document:created.description: Documento creato
|
||||
webhooks.events.documents.document:deleted.description: Documento eliminato
|
||||
webhooks.events.documents.document:updated.description: Documento aggiornato
|
||||
webhooks.events.documents.document:tag:added.description: Un tag è stato aggiunto a un documento
|
||||
webhooks.events.documents.document:tag:removed.description: Un tag è stato rimosso da un documento
|
||||
|
||||
# Navigation
|
||||
|
||||
layout.menu.home: Home
|
||||
layout.menu.documents: Documenti
|
||||
layout.menu.tags: Tag
|
||||
layout.menu.tagging-rules: Regole di tagging
|
||||
layout.menu.deleted-documents: Documenti eliminati
|
||||
layout.menu.organization-settings: Impostazioni
|
||||
layout.menu.api-keys: Chiavi API
|
||||
layout.menu.settings: Impostazioni
|
||||
layout.menu.account: Account
|
||||
layout.menu.general-settings: Impostazioni generali
|
||||
layout.menu.intake-emails: Email di acquisizione
|
||||
layout.menu.webhooks: Webhook
|
||||
layout.menu.members: Membri
|
||||
layout.menu.invitations: Inviti
|
||||
|
||||
layout.theme.light: Modalità chiara
|
||||
layout.theme.dark: Modalità scura
|
||||
layout.theme.system: Modalità sistema
|
||||
|
||||
layout.search.placeholder: Cerca...
|
||||
layout.menu.import-document: Importa un documento
|
||||
|
||||
user-menu.account-settings: Impostazioni account
|
||||
user-menu.api-keys: Chiavi API
|
||||
user-menu.invitations: Inviti
|
||||
user-menu.language: Lingua
|
||||
user-menu.logout: Esci
|
||||
|
||||
# Command palette
|
||||
|
||||
command-palette.search.placeholder: Cerca comandi o documenti
|
||||
command-palette.no-results: Nessun risultato trovato
|
||||
command-palette.sections.documents: Documenti
|
||||
command-palette.sections.theme: Tema
|
||||
|
||||
# API errors
|
||||
|
||||
api-errors.document.already_exists: Il documento esiste già
|
||||
api-errors.document.file_too_big: Il file del documento è troppo grande
|
||||
api-errors.intake_email.limit_reached: È stato raggiunto il numero massimo di email di acquisizione per questa organizzazione. Aggiorna il tuo piano per crearne altre.
|
||||
api-errors.user.max_organization_count_reached: Hai raggiunto il numero massimo di organizzazioni che puoi creare, se hai bisogno di crearne altre contatta il supporto.
|
||||
api-errors.default: Si è verificato un errore durante l'elaborazione della richiesta.
|
||||
api-errors.organization.invitation_already_exists: Esiste già un invito per questa email in questa organizzazione.
|
||||
api-errors.user.already_in_organization: Questo utente è già in questa organizzazione.
|
||||
api-errors.user.organization_invitation_limit_reached: È stato raggiunto il numero massimo di inviti per oggi. Riprova domani.
|
||||
api-errors.demo.not_available: Questa funzionalità non è disponibile nella demo
|
||||
api-errors.tags.already_exists: Esiste già un tag con questo nome per questa organizzazione
|
||||
api-errors.internal.error: Si è verificato un errore durante l'elaborazione della richiesta. Riprova.
|
||||
api-errors.auth.invalid_origin: Origine dell'applicazione non valida. Se stai ospitando Papra, assicurati che la variabile di ambiente APP_BASE_URL corrisponda all'URL corrente. Per maggiori dettagli, consulta https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# Not found
|
||||
|
||||
not-found.title: 404 - Non trovato
|
||||
not-found.description: Spiacenti, la pagina che stai cercando non sembra esistere. Controlla l'URL e riprova.
|
||||
not-found.back-to-home: Torna alla home
|
||||
|
||||
# Demo
|
||||
|
||||
demo.popup.description: Questo è un ambiente demo, tutti i dati vengono salvati nello storage locale del browser.
|
||||
demo.popup.discord: Unisciti a {{ discordLink }} per ricevere supporto, proporre funzionalità o semplicemente fare due chiacchiere.
|
||||
demo.popup.discord-link-label: Server Discord
|
||||
demo.popup.reset: Reimposta dati demo
|
||||
demo.popup.hide: Nascondi
|
||||
|
||||
# Color picker
|
||||
|
||||
color-picker.hue: Tonalità
|
||||
color-picker.saturation: Saturazione
|
||||
color-picker.lightness: Luminosità
|
||||
color-picker.select-color: Seleziona colore
|
||||
color-picker.select-a-color: Seleziona un colore
|
||||
571
apps/papra-client/src/locales/pl.yml
Normal file
571
apps/papra-client/src/locales/pl.yml
Normal file
@@ -0,0 +1,571 @@
|
||||
# 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.title: Zdarzenia dokumentów
|
||||
webhooks.events.documents.document:created.description: Utworzono dokument
|
||||
webhooks.events.documents.document:deleted.description: Usunięto dokument
|
||||
webhooks.events.documents.document:updated.description: Dokument został zaktualizowany
|
||||
webhooks.events.documents.document:tag:added.description: Tag został dodany do dokumentu
|
||||
webhooks.events.documents.document:tag:removed.description: Tag został usunięty z dokumentu
|
||||
|
||||
# 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
|
||||
api-errors.internal.error: Wystąpił błąd podczas przetwarzania żądania. Spróbuj ponownie później.
|
||||
api-errors.auth.invalid_origin: Nieprawidłowa lokalizacja aplikacji. Jeśli hostujesz Papra, upewnij się, że zmienna środowiskowa APP_BASE_URL odpowiada bieżącemu adresowi URL. Aby uzyskać więcej informacji, zobacz https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# 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
|
||||
571
apps/papra-client/src/locales/pt-BR.yml
Normal file
571
apps/papra-client/src/locales/pt-BR.yml
Normal file
@@ -0,0 +1,571 @@
|
||||
# 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.title: Eventos de documentos
|
||||
webhooks.events.documents.document:created.description: Documento criado
|
||||
webhooks.events.documents.document:deleted.description: Documento excluído
|
||||
webhooks.events.documents.document:updated.description: Documento atualizado
|
||||
webhooks.events.documents.document:tag:added.description: Uma tag foi adicionada a um documento
|
||||
webhooks.events.documents.document:tag:removed.description: Uma tag foi removida de um documento
|
||||
|
||||
# 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
|
||||
api-errors.internal.error: Ocorreu um erro ao processar sua solicitação. Por favor, tente novamente.
|
||||
api-errors.auth.invalid_origin: Origem da aplicação inválida. Se você está hospedando o Papra, certifique-se de que a variável de ambiente APP_BASE_URL corresponde à sua URL atual. Para mais detalhes, consulte https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# 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
|
||||
571
apps/papra-client/src/locales/pt.yml
Normal file
571
apps/papra-client/src/locales/pt.yml
Normal file
@@ -0,0 +1,571 @@
|
||||
# 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
|
||||
|
||||
# auth.no-auth-provider.title: No authentication provider
|
||||
# auth.no-auth-provider.description: There are no authentication providers enabled on this instance of Papra. Please contact the administrator of this instance to enable them.
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: 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.title: Eventos de documentos
|
||||
webhooks.events.documents.document:created.description: Documento criado
|
||||
webhooks.events.documents.document:deleted.description: Documento eliminado
|
||||
webhooks.events.documents.document:updated.description: Documento atualizado
|
||||
webhooks.events.documents.document:tag:added.description: Uma etiqueta foi adicionada a um documento
|
||||
webhooks.events.documents.document:tag:removed.description: Uma etiqueta foi removida de um documento
|
||||
|
||||
# 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
|
||||
api-errors.internal.error: Ocorreu um erro ao processar a solicitação. Por favor, tente novamente.
|
||||
api-errors.auth.invalid_origin: Origem da aplicação inválida. Se você está hospedando o Papra, certifique-se de que a variável de ambiente APP_BASE_URL corresponde à sua URL atual. Para mais detalhes, consulte https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# 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
|
||||
571
apps/papra-client/src/locales/ro.yml
Normal file
571
apps/papra-client/src/locales/ro.yml
Normal file
@@ -0,0 +1,571 @@
|
||||
# Authentication
|
||||
|
||||
auth.request-password-reset.title: Resetează parola
|
||||
auth.request-password-reset.description: Introdu adresa de e-mail pentru a reseta parola.
|
||||
auth.request-password-reset.requested: Dacă există un cont pentru acest e-mail, am trimis un e-mail pentru resetarea parolei.
|
||||
auth.request-password-reset.back-to-login: Înapoi la autentificare
|
||||
auth.request-password-reset.form.email.label: E-mail
|
||||
auth.request-password-reset.form.email.placeholder: 'Exemplu: popescu@papra.app'
|
||||
auth.request-password-reset.form.email.required: Introdu adresa de e-mail
|
||||
auth.request-password-reset.form.email.invalid: Adresa de e-mail este invalidă
|
||||
auth.request-password-reset.form.submit: Trimite cererea de resetare a parolei
|
||||
|
||||
auth.reset-password.title: Resetează parola
|
||||
auth.reset-password.description: Introdu o parolă noua pentră a o reseta pe cea veche.
|
||||
auth.reset-password.reset: Parola a fost resetată cu success.
|
||||
auth.reset-password.back-to-login: Înapoi la autentificare
|
||||
auth.reset-password.form.new-password.label: Parolă nouă
|
||||
auth.reset-password.form.new-password.placeholder: 'Exemplu: **********'
|
||||
auth.reset-password.form.new-password.required: Introdu parola nouă
|
||||
auth.reset-password.form.new-password.min-length: Parola trebuie să fie de minim {{ minLength }} caractere
|
||||
auth.reset-password.form.new-password.max-length: Parola trebuie să fie de maxim {{ maxLength }} de caractere
|
||||
auth.reset-password.form.submit: Resetează parola
|
||||
|
||||
auth.email-provider.open: Deschide {{ provider }}
|
||||
|
||||
auth.login.title: Autentificare la Papra
|
||||
auth.login.description: Introdu e-mailul sau folosește autentificarea cu cont social pentru a accesa contul Papra.
|
||||
auth.login.login-with-provider: Autentificare cu {{ provider }}
|
||||
auth.login.no-account: Nu ai cont?
|
||||
auth.login.register: Înregistrare
|
||||
auth.login.form.email.label: E-mail
|
||||
auth.login.form.email.placeholder: 'Exemplu: popescu@papra.app'
|
||||
auth.login.form.email.required: Introdu adresa de e-mail
|
||||
auth.login.form.email.invalid: Adresa e-mail este invalidă
|
||||
auth.login.form.password.label: Parola
|
||||
auth.login.form.password.placeholder: Setează o parola noua
|
||||
auth.login.form.password.required: Introdu parola noua
|
||||
auth.login.form.remember-me.label: Ține-mă minte
|
||||
auth.login.form.forgot-password.label: Ai uitat parola?
|
||||
auth.login.form.submit: Autentificare
|
||||
|
||||
auth.register.title: Înregistrare la Papra
|
||||
auth.register.description: Introdu e-mailul pentru a accesa Papra.
|
||||
auth.register.register-with-email: înregistrează-te cu e-mail
|
||||
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: Autentificare
|
||||
auth.register.registration-disabled.title: Înregistrarea este dezactivată
|
||||
auth.register.registration-disabled.description: Crearea de conturi noi este momentan dezactivată pe această instanță de Papra. Doar utilizatorii cu conturi existente se pot autentifica. Dacă aceasta pare a fi o greșeală, contactează administratorul acestei instanțe.
|
||||
auth.register.form.email.label: E-mail
|
||||
auth.register.form.email.placeholder: 'Exemplu: popescu@papra.app'
|
||||
auth.register.form.email.required: Introdu adresa de e-mail
|
||||
auth.register.form.email.invalid: Adresa e-mail este invalida
|
||||
auth.register.form.password.label: Parola
|
||||
auth.register.form.password.placeholder: Setează parola
|
||||
auth.register.form.password.required: Te rugăm să introduci parola
|
||||
auth.register.form.password.min-length: Parola trebuie să fie de minim {{ minLength }} caractere
|
||||
auth.register.form.password.max-length: Parola trebuie să fie de maxim {{ maxLength }} de caractere
|
||||
auth.register.form.name.label: Nume
|
||||
auth.register.form.name.placeholder: 'Exemplu: Andrei Popescu'
|
||||
auth.register.form.name.required: Introdu numele
|
||||
auth.register.form.name.max-length: Numele trebuie să fie de minim {{ maxLength }} caractere
|
||||
auth.register.form.submit: Înregistrare
|
||||
|
||||
auth.email-validation-required.title: Verifică-ți email-ul
|
||||
auth.email-validation-required.description: A fost trimis un e-mail de verificare la adresa ta de e-mail. Te rugăm să îți verifici adresa de e-mail dând click pe linkul din e-mail.
|
||||
|
||||
auth.legal-links.description: Continuând, confirmați că întelegeți și sunteti de acord cu {{ terms }} și {{ privacy }}.
|
||||
auth.legal-links.terms: Termenii și condițiile
|
||||
auth.legal-links.privacy: Politica de confidențialitate
|
||||
|
||||
auth.no-auth-provider.title: Niciun furnizor de autentificare
|
||||
auth.no-auth-provider.description: Nu este niciun furnizor de autentificare activat pe această instanță de Papra. Te rugăm să contactezi administratorul aceste instanțe pentru a le activa.
|
||||
|
||||
# User settings
|
||||
|
||||
user.settings.title: Setările utilizatorului
|
||||
user.settings.description: Configurează setările contului aici.
|
||||
|
||||
user.settings.email.title: Adresa de e-mail
|
||||
user.settings.email.description: Adresa de e-mail nu poate fi schimbată.
|
||||
user.settings.email.label: Adresa de e-mail
|
||||
|
||||
user.settings.name.title: Numele complet
|
||||
user.settings.name.description: Numele complet este afișat altor membri din organizație.
|
||||
user.settings.name.label: Numele complet
|
||||
user.settings.name.placeholder: Ex. Andrei Popescu
|
||||
user.settings.name.update: Schimbă numele
|
||||
user.settings.name.updated: Numele a fost schimbat
|
||||
|
||||
user.settings.logout.title: Deconectare
|
||||
user.settings.logout.description: Vei fi deconectat din cont. Te poți conecta înapoi ulterior.
|
||||
user.settings.logout.button: Deconectare
|
||||
|
||||
# Organizations
|
||||
|
||||
organizations.list.title: Organizațiile tale
|
||||
organizations.list.description: Organizațiile sunt o modalitate de a grupa documentele și de a gestiona accesul la acestea. Poți crea multiple organizații și invita membrii echipei tale să colaboreze.
|
||||
organizations.list.create-new: Creează o organizație nouă
|
||||
|
||||
organizations.details.no-documents.title: Niciun document
|
||||
organizations.details.no-documents.description: Încă nu există documente în această organizație. Încarcă niște documente pentru a începe.
|
||||
organizations.details.upload-documents: Încarcă documente
|
||||
organizations.details.documents-count: documente in total
|
||||
organizations.details.total-size: mărime totala
|
||||
organizations.details.latest-documents: Ultimele documente încarcate
|
||||
|
||||
organizations.create.title: Creează o organizație nouă
|
||||
organizations.create.description: Documentele sunt grupate în funcție de organizație. Poți crea mai multe organizații pentru documente diferite, de exemplu, pentru uz personal și profesional.
|
||||
organizations.create.back: Înapoi
|
||||
organizations.create.error.max-count-reached: Ai ajuns la numărul maxim de organizații pe care le poți crea. Dacă ai nevoie de mai multe, contactează asistența.
|
||||
organizations.create.form.name.label: Numle organizației
|
||||
organizations.create.form.name.placeholder: Ex. Acme SRL.
|
||||
organizations.create.form.name.required: Introdu numele organizației
|
||||
organizations.create.form.submit: Creează organizația
|
||||
organizations.create.success: Organizația a fost creată cu succes
|
||||
|
||||
organizations.create-first.title: Creează organizația
|
||||
organizations.create-first.description: Documentele sunt grupate în funcție de organizație. Poți crea mai multe organizații pentru documente diferite, de exemplu, pentru uz personal și profesional.
|
||||
organizations.create-first.default-name: Organizația mea
|
||||
organizations.create-first.user-name: 'Organizația lui {{ name }}'
|
||||
|
||||
organization.settings.title: Setările organizației
|
||||
organization.settings.page.title: Setările organizației
|
||||
organization.settings.page.description: Gestionează setarile organizației aici.
|
||||
organization.settings.name.title: Numele organizației
|
||||
organization.settings.name.update: Actualizează numele
|
||||
organization.settings.name.placeholder: Ex. Acme SRL.
|
||||
organization.settings.name.updated: Numele organizației a fost actualizat
|
||||
organization.settings.subscription.title: Abonament
|
||||
organization.settings.subscription.description: Gestionează facturile și metodele de plată.
|
||||
organization.settings.subscription.manage: Gestionează-ți abonamentul
|
||||
organization.settings.subscription.error: Eroare la obținerea URL-ului portalului de client
|
||||
organization.settings.delete.title: Șterge organizația
|
||||
organization.settings.delete.description: Ștergerea acestei organizații va elimina definitiv toate datele asociate cu aceasta.
|
||||
organization.settings.delete.confirm.title: Șterge organizatia
|
||||
organization.settings.delete.confirm.message: Ești sigur că vrei să ștergi această organizație? Aceasta operatie nu poate fi anulată si toate datele asociate cu aceasta vor fi eliminate definitiv.
|
||||
organization.settings.delete.confirm.confirm-button: Șterge organizație
|
||||
organization.settings.delete.confirm.cancel-button: Anulează
|
||||
organization.settings.delete.success: Organizație ștearsă cu succes
|
||||
|
||||
organizations.members.title: Membri
|
||||
organizations.members.description: Gestionează membrii organizației tale
|
||||
organizations.members.invite-member: Invită membru
|
||||
organizations.members.invite-member-disabled-tooltip: Doar administratorii sau proprietarii pot invita membrii la organizație
|
||||
organizations.members.remove-from-organization: Elimina din organizație
|
||||
organizations.members.role: Rol
|
||||
organizations.members.roles.owner: Proprietar
|
||||
organizations.members.roles.admin: Admin
|
||||
organizations.members.roles.member: Membru
|
||||
organizations.members.delete.confirm.title: Elimină membrul
|
||||
organizations.members.delete.confirm.message: Ești sigur că vrei să elimini acest membru din organizație?
|
||||
organizations.members.delete.confirm.confirm-button: Elimină
|
||||
organizations.members.delete.confirm.cancel-button: Anulează
|
||||
organizations.members.delete.success: Membru eliminat cu succes
|
||||
organizations.members.update-role.success: Rolul membrului a fost actualizat
|
||||
organizations.members.table.headers.name: Nume
|
||||
organizations.members.table.headers.email: E-mail
|
||||
organizations.members.table.headers.role: Rol
|
||||
organizations.members.table.headers.created: Creat
|
||||
organizations.members.table.headers.actions: Acțiuni
|
||||
|
||||
organizations.invite-member.title: Invită membru
|
||||
organizations.invite-member.description: Invită un membru la organizație
|
||||
organizations.invite-member.form.email.label: E-mail
|
||||
organizations.invite-member.form.email.placeholder: 'Exemplu: ada@papra.app'
|
||||
organizations.invite-member.form.email.required: Introdu o adresă de e-mail validă
|
||||
organizations.invite-member.form.role.label: Rol
|
||||
organizations.invite-member.form.submit: Invită membru
|
||||
organizations.invite-member.success.message: Membru invitat
|
||||
organizations.invite-member.success.description: Adresă de e-mail a fost invitată la organizație.
|
||||
organizations.invite-member.error.message: Eroare la invitarea membrului
|
||||
|
||||
organizations.invitations.title: Invitații
|
||||
organizations.invitations.description: Gestionează invitațiile la organizație
|
||||
organizations.invitations.list.cta: Invită membru
|
||||
organizations.invitations.list.empty.title: Nicio invitație în așteptare
|
||||
organizations.invitations.list.empty.description: Încă nu ai fost invitat la nicio organizație.
|
||||
organizations.invitations.status.pending: În așteptare
|
||||
organizations.invitations.status.accepted: Acceptată
|
||||
organizations.invitations.status.rejected: Respinsă
|
||||
organizations.invitations.status.expired: Expirată
|
||||
organizations.invitations.status.cancelled: Anulată
|
||||
organizations.invitations.resend: Retrimite invitația
|
||||
organizations.invitations.cancel.title: Anulează invitația
|
||||
organizations.invitations.cancel.description: Ești sigur că vrei să anulezi această invitație?
|
||||
organizations.invitations.cancel.confirm: Anulează invitația
|
||||
organizations.invitations.cancel.cancel: Anulează
|
||||
organizations.invitations.resend.title: Retrimite invitația
|
||||
organizations.invitations.resend.description: Ești sigur că vrei să retrimiți această invitație? Se va trimite un nou e-mail destinatarului.
|
||||
organizations.invitations.resend.confirm: Retrimite invitația
|
||||
organizations.invitations.resend.cancel: Anulează
|
||||
|
||||
invitations.list.title: Invitații
|
||||
invitations.list.description: Gestionează invitații la organizație
|
||||
invitations.list.empty.title: Nicio invitație în așteptare
|
||||
invitations.list.empty.description: Încă nu ai fost invitat la nicio organizație.
|
||||
invitations.list.headers.organization: Organizație
|
||||
invitations.list.headers.status: Status
|
||||
invitations.list.headers.created: Creat la
|
||||
invitations.list.headers.actions: Acțiuni
|
||||
invitations.list.actions.accept: Acceptă
|
||||
invitations.list.actions.reject: Refuză
|
||||
invitations.list.actions.accept.success.message: Invitație acceptată
|
||||
invitations.list.actions.accept.success.description: Invitația a fost acceptată.
|
||||
invitations.list.actions.reject.success.message: Invitație refuzată
|
||||
invitations.list.actions.reject.success.description: Invitația a fost refuzată.
|
||||
|
||||
# Documents
|
||||
|
||||
documents.list.title: Documente
|
||||
documents.list.no-documents.title: Niciun document
|
||||
documents.list.no-documents.description: Încă nu există documente în aceasta organizație. Începe prin a încarca câteva documente.
|
||||
documents.list.no-results: Nu au fost găsite documente
|
||||
|
||||
documents.tabs.info: Info
|
||||
documents.tabs.content: Conținut
|
||||
documents.tabs.activity: Activitate
|
||||
documents.deleted.message: Acest document a fost șters și va fi eliminat definitiv după {{ days }} zile.
|
||||
documents.actions.download: Descarcă
|
||||
documents.actions.open-in-new-tab: Deschide în filă nouă
|
||||
documents.actions.restore: Restaurează
|
||||
documents.actions.delete: Șterge
|
||||
documents.actions.edit: Editează
|
||||
documents.actions.cancel: Anulează
|
||||
documents.actions.save: Salvează
|
||||
documents.actions.saving: Se salvează...
|
||||
documents.content.alert: Conținutul documentului este extras automat din document la încarcare. Este folosit doar pentru căutare și 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: Niciodată
|
||||
|
||||
documents.rename.title: Redenumește documentul
|
||||
documents.rename.form.name.label: Nume
|
||||
documents.rename.form.name.placeholder: 'Exemplu: Factura 2024'
|
||||
documents.rename.form.name.required: Te rugăm să introduci un nume pentru document
|
||||
documents.rename.form.name.max-length: Numele trebuie să aibă mai puțin de 255 de caractere
|
||||
documents.rename.form.submit: Redenumește documentul
|
||||
documents.rename.success: Document redenumit cu succes
|
||||
documents.rename.cancel: Anulează
|
||||
|
||||
import-documents.title.error: '{{ count }} documente au eșuat'
|
||||
import-documents.title.success: '{{ count }} documente importate'
|
||||
import-documents.title.pending: '{{ count }} / {{ total }} documente importate'
|
||||
import-documents.title.none: Importă documente
|
||||
import-documents.no-import-in-progress: Niciun import de documente în curs
|
||||
|
||||
documents.deleted.title: Documente șterse
|
||||
documents.deleted.empty.title: Niciun document șters
|
||||
documents.deleted.empty.description: Nu ai niciun document șters. Documentele care sunt șterse vor fi mutate în coșul de gunoi timp de {{ days }} zile.
|
||||
documents.deleted.retention-notice: Toate documentele șterse sunt stocate în coșul de gunoi timp de {{ days }} zile. După acest interval, documentele vor fi șterse definitiv și nu le vei mai putea restaura.
|
||||
documents.deleted.deleted-at: Șterse la
|
||||
documents.deleted.restoring: Se restaurează...
|
||||
documents.deleted.deleting: Se șterge...
|
||||
|
||||
documents.preview.unknown-file-type: Nicio previzualizare disponibilă pentru acest tip de fișier
|
||||
documents.preview.binary-file: Acesta pare a fi un fișier binar și nu poate fi afișat ca text
|
||||
|
||||
trash.delete-all.button: Șterge tot
|
||||
trash.delete-all.confirm.title: Ștergi definitiv toate documentele?
|
||||
trash.delete-all.confirm.description: Ești sigur că dorești să ștergi definitiv toate documentele din coșul de gunoi? Această acțiune nu poate fi anulată.
|
||||
trash.delete-all.confirm.label: Șterge
|
||||
trash.delete-all.confirm.cancel: Anulează
|
||||
trash.delete.button: Șterge
|
||||
trash.delete.confirm.title: Ștergi definitiv documentul?
|
||||
trash.delete.confirm.description: Sunteti sigur ca doriti să stergeti definitiv acest document din cosul de gunoi? Această actiune nu poate fi anulată.
|
||||
trash.delete.confirm.label: Șterge
|
||||
trash.delete.confirm.cancel: Anulează
|
||||
trash.deleted.success.title: Document șters
|
||||
trash.deleted.success.description: Documentul a fost șters definitiv.
|
||||
|
||||
activity.document.created: Documentul a fost creat
|
||||
activity.document.updated.single: Câmpul {{ field }} a fost actualizat
|
||||
activity.document.updated.multiple: Câmpurile {{ fields }} au fost actualizate
|
||||
activity.document.updated: Documentul a fost actualizat
|
||||
activity.document.deleted: Documentul a fost șters
|
||||
activity.document.restored: Documentul a fost restaurat
|
||||
activity.document.tagged: Eticheta {{ tag }} a fost adaugată
|
||||
activity.document.untagged: Eticheta {{ tag }} a fost eliminată
|
||||
|
||||
activity.document.user.name: de {{ name }}
|
||||
|
||||
activity.load-more: Încarcă mai multe
|
||||
activity.no-more-activities: Nu mai sunt activități pentru acest document
|
||||
|
||||
# Tags
|
||||
|
||||
tags.no-tags.title: Încă nu există etichete
|
||||
tags.no-tags.description: Această organizație nu are încă etichete. Etichetele sunt folosite pentru a clasifica documentele. Poți adăuga etichete la documente pentru a le găsi și organiza mai ușor.
|
||||
tags.no-tags.create-tag: Creează eticheta
|
||||
|
||||
tags.title: Etichete documente
|
||||
tags.description: Etichetele sunt folosite pentru a clasifica documentele. Poți adăuga etichete la documente pentru a le găsi și organiza mai ușor.
|
||||
tags.create: Creează eticheta
|
||||
tags.update: Actualizează eticheta
|
||||
tags.delete: Șterge eticheta
|
||||
tags.delete.confirm.title: Șterge eticheta
|
||||
tags.delete.confirm.message: Ești sigur că vrei să ștergi aceasta eticheta? Stergerea unei etichete o va elimina din toate documentele.
|
||||
tags.delete.confirm.confirm-button: Șterge
|
||||
tags.delete.confirm.cancel-button: Anulează
|
||||
tags.delete.success: Eticheta a fost ștearsă cu succes
|
||||
tags.create.success: Eticheta "{{ name }}" a fost creată cu succes.
|
||||
tags.update.success: Eticheta "{{ name }}" a fost actualizată cu succes.
|
||||
tags.form.name.label: Nume
|
||||
tags.form.name.placeholder: Ex. Contracte
|
||||
tags.form.name.required: Te rugăm să introduci un nume pentru etichetă
|
||||
tags.form.name.max-length: Numele etichetei trebuie să aibă mai puțin de 64 de caractere
|
||||
tags.form.color.label: Culoare
|
||||
tags.form.color.required: Te rugăm să introduci o culoare
|
||||
tags.form.color.invalid: Culoarea hex este formatată greșit.
|
||||
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 să aibă mai puțin de 256 de caractere
|
||||
tags.form.no-description: Nicio descriere
|
||||
tags.table.headers.tag: Etichetă
|
||||
tags.table.headers.description: Descriere
|
||||
tags.table.headers.documents: Documente
|
||||
tags.table.headers.created: Creat la
|
||||
tags.table.headers.actions: Acțiuni
|
||||
|
||||
# Tagging rules
|
||||
|
||||
tagging-rules.field.name: nume document
|
||||
tagging-rules.field.content: conținut document
|
||||
tagging-rules.operator.equals: egal cu
|
||||
tagging-rules.operator.not-equals: nu este egal cu
|
||||
tagging-rules.operator.contains: conține
|
||||
tagging-rules.operator.not-contains: nu conține
|
||||
tagging-rules.operator.starts-with: începe cu
|
||||
tagging-rules.operator.ends-with: se termină cu
|
||||
tagging-rules.list.title: Reguli de etichetare
|
||||
tagging-rules.list.description: Gestionează regulile de etichetare ale organizației pentru a eticheta automat documentele pe baza unor condiții definite.
|
||||
tagging-rules.list.demo-warning: 'Notă: Deoarece acesta este un mediu demonstrativ (fără server), regulile de etichetare nu vor fi aplicate documentelor nou adăugate.'
|
||||
tagging-rules.list.no-tagging-rules.title: Nicio regulă de etichetare
|
||||
tagging-rules.list.no-tagging-rules.description: Creează o regulă de etichetare pentru a eticheta automat documentele adăugate pe baza unor condiții definite.
|
||||
tagging-rules.list.no-tagging-rules.create-tagging-rule: Creează regula de etichetare
|
||||
tagging-rules.list.card.no-conditions: Nicio condiție
|
||||
tagging-rules.list.card.one-condition: O condiție
|
||||
tagging-rules.list.card.conditions: '{{ count }} condiții'
|
||||
tagging-rules.list.card.delete: Șterge regula
|
||||
tagging-rules.list.card.edit: Editează regula
|
||||
tagging-rules.create.title: Creează regula de etichetare
|
||||
tagging-rules.create.success: Regula de etichetare a fost creată cu succes
|
||||
tagging-rules.create.error: Nu s-a putut crea regula de etichetare
|
||||
tagging-rules.create.submit: Creează regula
|
||||
tagging-rules.form.name.label: Nume
|
||||
tagging-rules.form.name.placeholder: 'Exemplu: Etichetează facturile'
|
||||
tagging-rules.form.name.min-length: Te rugăm să introduci numele regulii
|
||||
tagging-rules.form.name.max-length: Numele trebuie să aibă mai puțin de 64 de caractere
|
||||
tagging-rules.form.description.label: Descriere
|
||||
tagging-rules.form.description.placeholder: "Exemplu: Etichetează documentele cu 'factură' în nume"
|
||||
tagging-rules.form.description.max-length: Descrierea trebuie să aibă mai puțin de 256 de caractere
|
||||
tagging-rules.form.conditions.label: Condiții
|
||||
tagging-rules.form.conditions.description: Definește condițiile care trebuie îndeplinite pentru ca regula să se aplice. Toate condițiile trebuie îndeplinite pentru ca regula să se aplice.
|
||||
tagging-rules.form.conditions.add-condition: Adaugă condiție
|
||||
tagging-rules.form.conditions.no-conditions.title: Nicio condiție
|
||||
tagging-rules.form.conditions.no-conditions.description: Nu ai adăugat nicio condiție acestei reguli. Această regula va aplica etichetele sale tuturor documentelor.
|
||||
tagging-rules.form.conditions.no-conditions.confirm: Aplică regula fara condiții
|
||||
tagging-rules.form.conditions.no-conditions.cancel: Anulează
|
||||
tagging-rules.form.conditions.value.placeholder: 'Exemplu: factură'
|
||||
tagging-rules.form.conditions.value.min-length: Te rugăm să introduci o valoare pentru condiție
|
||||
tagging-rules.form.tags.label: Etichete
|
||||
tagging-rules.form.tags.description: Selectează etichetele de aplicat documentelor adăugate care corespund condițiilor
|
||||
tagging-rules.form.tags.min-length: Este necesară cel puțin o etichetă de aplicat
|
||||
tagging-rules.form.tags.add-tag: Creează eticheta
|
||||
tagging-rules.form.submit: Creează regula
|
||||
tagging-rules.update.title: Actualizează regula de etichetare
|
||||
tagging-rules.update.error: Nu s-a putut actualiza regula de etichetare
|
||||
tagging-rules.update.submit: Actualizează regula
|
||||
tagging-rules.update.cancel: Anulează
|
||||
|
||||
# Intake emails
|
||||
|
||||
intake-emails.title: E-mailuri de primire
|
||||
intake-emails.description: Adresele de e-mail de primire sunt folosite pentru a introduce automat email-uri în Papra. Doar redirecționează e-mailuri către adresa de primire, iar fișierele atașate vor fi adăugate automat în documentele organizației tale.
|
||||
intake-emails.disabled.title: Email-urile de primire sunt dezactivate
|
||||
intake-emails.disabled.description: Email-urile de primire sunt dezactivate pe aceasta instanță. Te rugăm să contactezi administratorul pentru a le activa. Consultă {{ documentation }} pentru mai multe informații.
|
||||
intake-emails.disabled.documentation: documentația
|
||||
intake-emails.info: Vor fi procesate numai e-mailurile de primire activate de la originile permise. Poți activa sau dezactiva un e-mail de primire în orice moment.
|
||||
intake-emails.empty.title: Niciun e-mail de primire
|
||||
intake-emails.empty.description: Generează o adresă de primire pentru a primi cu ușurință fișiere atașate din e-mail.
|
||||
intake-emails.empty.generate: Generează e-mail de primire
|
||||
intake-emails.count: '{{ count }} email{{ plural }} de primire pentru această organizație'
|
||||
intake-emails.new: E-mail nou de primire
|
||||
intake-emails.disabled-label: (Dezactivat)
|
||||
intake-emails.no-origins: Nicio origine de e-mail permisă
|
||||
intake-emails.allowed-origins: Permis de la {{ count }} adrese{{ plural }}
|
||||
intake-emails.actions.enable: Activează
|
||||
intake-emails.actions.disable: Dezactivează
|
||||
intake-emails.actions.manage-origins: Gestionează adresele de origine
|
||||
intake-emails.actions.delete: Șterge
|
||||
intake-emails.delete.confirm.title: Ștergi email-ul de primire?
|
||||
intake-emails.delete.confirm.message: Ești sigur că vrei să ștergi acest e-mail de primire? Această acțiune nu poate fi anulată.
|
||||
intake-emails.delete.confirm.confirm-button: Șterge email-ul de primire
|
||||
intake-emails.delete.confirm.cancel-button: Anulează
|
||||
intake-emails.delete.success: E-mail de primire șters
|
||||
intake-emails.create.success: E-mail de primire creat
|
||||
intake-emails.update.success.enabled: E-mail de primire activat
|
||||
intake-emails.update.success.disabled: E-mail de primire dezactivat
|
||||
intake-emails.allowed-origins.title: Origini permise
|
||||
intake-emails.allowed-origins.description: Doar email-urile trimise la {{ e-mail }} de la aceste origini vor fi procesate. Dacă nu sunt specificate origini, toate email-urile vor fi ignorate.
|
||||
intake-emails.allowed-origins.add.label: Adaugă adresa de e-mail de origine permisă
|
||||
intake-emails.allowed-origins.add.placeholder: Ex. ada@papra.app
|
||||
intake-emails.allowed-origins.add.button: Adaugă
|
||||
intake-emails.allowed-origins.add.error.exists: Acest e-mail este deja în originile permise pentru acest e-mail de primire
|
||||
|
||||
# API keys
|
||||
|
||||
api-keys.permissions.documents.title: Documente
|
||||
api-keys.permissions.documents.documents:create: Creează documente
|
||||
api-keys.permissions.documents.documents:read: Citește documente
|
||||
api-keys.permissions.documents.documents:update: Actualizează documente
|
||||
api-keys.permissions.documents.documents:delete: Șterge documente
|
||||
api-keys.permissions.tags.title: Etichete
|
||||
api-keys.permissions.tags.tags:create: Creează etichete
|
||||
api-keys.permissions.tags.tags:read: Citește etichete
|
||||
api-keys.permissions.tags.tags:update: Actualizează etichete
|
||||
api-keys.permissions.tags.tags:delete: Șterge etichete
|
||||
api-keys.create.title: Creează cheie API
|
||||
api-keys.create.description: Creează o nouă cheie API pentru a accesa API-ul Papra.
|
||||
api-keys.create.success: Cheia API a fost creată cu succes.
|
||||
api-keys.create.back: Înapoi 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: Te rugăm să introduci un nume pentru cheia API
|
||||
api-keys.create.form.permissions.label: Permisiuni
|
||||
api-keys.create.form.permissions.required: Te rugăm să selectezi cel puțin o permisiune
|
||||
api-keys.create.form.submit: Creează cheie API
|
||||
api-keys.create.created.title: Cheie API creată
|
||||
api-keys.create.created.description: Cheia API a fost creată cu succes. Salveaz-o într-un loc sigur, deoarece nu va fi afișată din nou.
|
||||
api-keys.list.title: Chei API
|
||||
api-keys.list.description: Gestionează-ți cheile API aici.
|
||||
api-keys.list.create: Creează cheie API
|
||||
api-keys.list.empty.title: Nicio cheie API
|
||||
api-keys.list.empty.description: Creează o cheie API pentru a accesa API-ul Papra.
|
||||
api-keys.list.card.last-used: Ultima utilizare
|
||||
api-keys.list.card.never: Niciodată
|
||||
api-keys.list.card.created: Creat la
|
||||
api-keys.delete.success: Cheia API a fost ștearsă cu succes
|
||||
api-keys.delete.confirm.title: Șterge cheia API
|
||||
api-keys.delete.confirm.message: Ești sigur ca vrei să ștergi aceasta cheie API? Această acțiune nu poate fi anulată.
|
||||
api-keys.delete.confirm.confirm-button: Șterge
|
||||
api-keys.delete.confirm.cancel-button: Anulează
|
||||
|
||||
# Webhooks
|
||||
|
||||
webhooks.list.title: Webhook-uri
|
||||
webhooks.list.description: Gestionează webhook-urile organizației tale
|
||||
webhooks.list.empty.title: Niciun webhook
|
||||
webhooks.list.empty.description: Creează primul webhook pentru a începe să primesti evenimente
|
||||
webhooks.list.create: Creează webhook
|
||||
webhooks.list.card.last-triggered: Ultima declanșare
|
||||
webhooks.list.card.never: Niciodată
|
||||
webhooks.list.card.created: Creat la
|
||||
webhooks.create.title: Creează webhook
|
||||
webhooks.create.description: Creează un nou webhook pentru a primi evenimente
|
||||
webhooks.create.success: Webhook creat cu succes
|
||||
webhooks.create.back: Înapoi
|
||||
webhooks.create.form.submit: Creează 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 puțin un eveniment
|
||||
webhooks.update.title: Editează webhook
|
||||
webhooks.update.description: Actualizează detaliile webhook-ului
|
||||
webhooks.update.success: Webhook actualizat cu succes
|
||||
webhooks.update.submit: Actualizează webhook
|
||||
webhooks.update.cancel: Anulează
|
||||
webhooks.update.form.secret.placeholder: Introdu un secret nou
|
||||
webhooks.update.form.secret.placeholder-redacted: '[Secret protejat]'
|
||||
webhooks.update.form.rotate-secret.button: Rotește secretul
|
||||
webhooks.delete.success: Webhook șters cu succes
|
||||
webhooks.delete.confirm.title: Șterge webhook
|
||||
webhooks.delete.confirm.message: Ești sigur ca vrei să ștergi acest webhook?
|
||||
webhooks.delete.confirm.confirm-button: Șterge
|
||||
webhooks.delete.confirm.cancel-button: Anulează
|
||||
|
||||
webhooks.events.documents.title: Evenimente documente
|
||||
webhooks.events.documents.document:created.description: Document creat
|
||||
webhooks.events.documents.document:deleted.description: Document șters
|
||||
webhooks.events.documents.document:updated.description: Document actualizat
|
||||
webhooks.events.documents.document:tag:added.description: O etichetă a fost adăugată la un document
|
||||
webhooks.events.documents.document:tag:removed.description: O etichetă a fost eliminată dintr-un document
|
||||
|
||||
# Navigation
|
||||
|
||||
layout.menu.home: Acasă
|
||||
layout.menu.documents: Documente
|
||||
layout.menu.tags: Etichete
|
||||
layout.menu.tagging-rules: Reguli de etichetare
|
||||
layout.menu.deleted-documents: Documente șterse
|
||||
layout.menu.organization-settings: Setări organizație
|
||||
layout.menu.api-keys: Chei API
|
||||
layout.menu.settings: Setări
|
||||
layout.menu.account: Cont
|
||||
layout.menu.general-settings: Setări generale
|
||||
layout.menu.intake-emails: Email-uri de primire
|
||||
layout.menu.webhooks: Webhook-uri
|
||||
layout.menu.members: Membri
|
||||
layout.menu.invitations: Invitații
|
||||
|
||||
layout.theme.light: Mod luminos
|
||||
layout.theme.dark: Mod intunecat
|
||||
layout.theme.system: Modul sistemului
|
||||
|
||||
layout.search.placeholder: Căutare...
|
||||
layout.menu.import-document: Importă un document
|
||||
|
||||
user-menu.account-settings: Setări cont
|
||||
user-menu.api-keys: Chei API
|
||||
user-menu.invitations: Invitații
|
||||
user-menu.language: Limbă
|
||||
user-menu.logout: Deconectare
|
||||
|
||||
# Command palette
|
||||
|
||||
command-palette.search.placeholder: Caută comenzi sau documente
|
||||
command-palette.no-results: Niciun rezultat gasit
|
||||
command-palette.sections.documents: Documente
|
||||
command-palette.sections.theme: Temă
|
||||
|
||||
# API errors
|
||||
|
||||
api-errors.document.already_exists: Documentul există deja
|
||||
api-errors.document.file_too_big: Fișierul documentului este prea mare
|
||||
api-errors.intake_email.limit_reached: Numărul maxim de email-uri de primire pentru această organizație a fost atins. Te rugăm să-ți îmbunătățești planul pentru a crea mai multe email-uri de primire.
|
||||
api-errors.user.max_organization_count_reached: Ai atins numărul maxim de organizații pe care le poți crea. Dacă ai nevoie să creezi mai multe, te rugăm să contactezi asistența.
|
||||
api-errors.default: A apărut o eroare la procesarea cererii.
|
||||
api-errors.organization.invitation_already_exists: O invitatie pentru acest e-mail există deja în această organizație.
|
||||
api-errors.user.already_in_organization: Acest utilizator este deja în această organizație.
|
||||
api-errors.user.organization_invitation_limit_reached: Numărul maxim de invitații a fost atins pentru astazi. Te rugăm să încerci din nou mâine.
|
||||
api-errors.demo.not_available: Această functie nu este disponibila în demo
|
||||
api-errors.tags.already_exists: O etichetă cu acest nume există deja pentru aceasta organizație
|
||||
api-errors.internal.error: A apărut o eroare la procesarea cererii. Te rugăm să încerci din nou.
|
||||
api-errors.auth.invalid_origin: Origine invalidă a aplicației. Dacă hospedezi Papra, asigură-te că variabila de mediu APP_BASE_URL corespunde URL-ului actual. Pentru mai multe detalii, consulta https://docs.papra.app/resources/troubleshooting/#invalid-application-origin
|
||||
|
||||
# Not found
|
||||
|
||||
not-found.title: 404 - Nu a fost gasit
|
||||
not-found.description: Ne pare rău, pagina pe care o cauți nu pare să existe. Te rugăm să verifici URL-ul și să încerci din nou.
|
||||
not-found.back-to-home: Înapoi la pagina principală
|
||||
|
||||
# Demo
|
||||
|
||||
demo.popup.description: Acesta este un mediu demonstrativ, toate datele sunt salvate in stocarea locală a browserului.
|
||||
demo.popup.discord: Alătură-te {{ discordLink }} pentru a obtine asistență, a propune funcționalități sau doar pentru a discuta.
|
||||
demo.popup.discord-link-label: serverului de Discord
|
||||
demo.popup.reset: Resetează datele demo
|
||||
demo.popup.hide: Ascunde
|
||||
|
||||
# Color picker
|
||||
|
||||
color-picker.hue: Nuanță
|
||||
color-picker.saturation: Saturație
|
||||
color-picker.lightness: Luminozitate
|
||||
color-picker.select-color: Selectează culoarea
|
||||
color-picker.select-a-color: Selectează o culoare
|
||||
@@ -49,12 +49,21 @@ export async function authWithProvider({ provider, config }: { provider: SsoProv
|
||||
const isCustomProvider = config.auth.providers.customs.some(({ providerId }) => providerId === provider.key);
|
||||
|
||||
if (isCustomProvider) {
|
||||
signIn.oauth2({
|
||||
const { error } = await signIn.oauth2({
|
||||
providerId: provider.key,
|
||||
callbackURL: config.baseUrl,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await signIn.social({ provider: provider.key as 'github' | 'google', callbackURL: config.baseUrl });
|
||||
const { error } = await signIn.social({ provider: provider.key as 'github' | 'google', callbackURL: config.baseUrl });
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,34 +1,47 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { createSignal, Match, Switch } from 'solid-js';
|
||||
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
|
||||
export const SsoProviderButton: Component<{ name: string; icon?: string; onClick: () => Promise<void>; label: string }> = (props) => {
|
||||
const [getIsLoading, setIsLoading] = createSignal(false);
|
||||
const [getError, setError] = createSignal<string | undefined>(undefined);
|
||||
const { getErrorMessage } = useI18nApiErrors();
|
||||
|
||||
const onClick = async () => {
|
||||
setIsLoading(true);
|
||||
await props.onClick();
|
||||
try {
|
||||
await props.onClick();
|
||||
} catch (error) {
|
||||
setError(getErrorMessage({ error }));
|
||||
// reset loading only in catch as the auth via sso can take a while before the redirection happens
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="secondary" class="block w-full flex items-center justify-center gap-2" onClick={onClick} disabled={getIsLoading()}>
|
||||
<>
|
||||
<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>
|
||||
<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?.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>
|
||||
<Match when={props.icon}>
|
||||
<img src={props.icon} alt={props.name} class="size-4.5" />
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
{props.label}
|
||||
</Button>
|
||||
{props.label}
|
||||
</Button>
|
||||
|
||||
{getError() && <p class="text-red-500">{getError()}</p>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
||||
import { Separator } from '@/modules/ui/components/separator';
|
||||
@@ -14,12 +15,14 @@ import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||
import { getEnabledSsoProviderConfigs, isEmailVerificationRequiredError } from '../auth.models';
|
||||
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 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { config } = useConfig();
|
||||
const { t } = useI18n();
|
||||
const { createI18nApiError } = useI18nApiErrors({ t });
|
||||
|
||||
const { form, Form, Field } = createForm({
|
||||
onSubmit: async ({ email, password, rememberMe }) => {
|
||||
@@ -30,7 +33,7 @@ export const EmailLoginForm: Component = () => {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
throw createI18nApiError({ error });
|
||||
}
|
||||
},
|
||||
schema: v.object({
|
||||
@@ -105,7 +108,7 @@ export const LoginPage: Component = () => {
|
||||
const { config } = useConfig();
|
||||
const { t } = useI18n();
|
||||
|
||||
const [getShowEmailLogin, setShowEmailLogin] = createSignal(false);
|
||||
const [getShowEmailLoginForm, setShowEmailLoginForm] = createSignal(false);
|
||||
|
||||
const loginWithProvider = async (provider: SsoProviderConfig) => {
|
||||
await authWithProvider({ provider, config });
|
||||
@@ -113,6 +116,10 @@ export const LoginPage: Component = () => {
|
||||
|
||||
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">
|
||||
@@ -120,17 +127,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 })}>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { Separator } from '@/modules/ui/components/separator';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
@@ -13,12 +14,15 @@ import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||
import { getEnabledSsoProviderConfigs } from '../auth.models';
|
||||
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 = () => {
|
||||
const { config } = useConfig();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useI18n();
|
||||
const { createI18nApiError } = useI18nApiErrors({ t });
|
||||
|
||||
const { form, Form, Field } = createForm({
|
||||
onSubmit: async ({ email, password, name }) => {
|
||||
const { error } = await signUp.email({
|
||||
@@ -29,7 +33,7 @@ export const EmailRegisterForm: Component = () => {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
throw createI18nApiError({ error });
|
||||
}
|
||||
|
||||
if (config.auth.isEmailVerificationRequired) {
|
||||
@@ -139,6 +143,10 @@ export const RegisterPage: Component = () => {
|
||||
|
||||
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">
|
||||
@@ -150,17 +158,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 })}>
|
||||
|
||||
@@ -58,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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ 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 {
|
||||
@@ -35,7 +36,6 @@ 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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -2,13 +2,14 @@ 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 { 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) => {
|
||||
@@ -19,6 +20,83 @@ 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));
|
||||
|
||||
@@ -34,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 = 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>
|
||||
@@ -48,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>
|
||||
);
|
||||
|
||||
@@ -41,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>
|
||||
@@ -232,18 +232,18 @@ export const DocumentPage: Component = () => {
|
||||
<div class="flex-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex items-center gap-2 group bg-transparent! px-0"
|
||||
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">
|
||||
<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"></div>
|
||||
<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>
|
||||
|
||||
@@ -354,7 +354,7 @@ export const DocumentPage: Component = () => {
|
||||
value: (
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex items-center gap-2 group bg-transparent! p-0 h-auto"
|
||||
class="flex items-center gap-2 group bg-transparent! p-0 h-auto text-left"
|
||||
onClick={() => openRenameDialog({
|
||||
documentId: getDocument().id,
|
||||
organizationId: params.organizationId,
|
||||
@@ -363,7 +363,7 @@ export const DocumentPage: Component = () => {
|
||||
>
|
||||
{getDocument().name}
|
||||
|
||||
<div class="i-tabler-pencil size-4 text-muted-foreground group-hover:text-foreground transition-colors"></div>
|
||||
<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',
|
||||
|
||||
@@ -2,4 +2,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' },
|
||||
{ key: 'it', name: 'Italiano' },
|
||||
] as const;
|
||||
|
||||
@@ -70,13 +70,14 @@ describe('i18n models', () => {
|
||||
expect(t('hello')).to.eql('Hello!');
|
||||
});
|
||||
|
||||
test('the translator returns the key if the key is not in the dictionary', () => {
|
||||
test('the translator returns undefined if the key is not in the dictionary', () => {
|
||||
const dictionary = {
|
||||
hello: 'Hello!',
|
||||
};
|
||||
const t = createTranslator({ getDictionary: () => dictionary });
|
||||
|
||||
expect(t('world' as any)).to.eql('world');
|
||||
expect(t('world' as any)).to.eql(undefined);
|
||||
expect(t('world' as any, { name: 'John' })).to.eql(undefined);
|
||||
});
|
||||
|
||||
test('the translator replaces the placeholders in the translation', () => {
|
||||
|
||||
@@ -36,15 +36,15 @@ export function createTranslator<Dict extends Record<string, string>>({ getDicti
|
||||
console.warn(`Translation not found for key: ${String(key)}`);
|
||||
}
|
||||
|
||||
let translation: string = translationFromDictionary ?? key;
|
||||
|
||||
if (args) {
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
translation = translation.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), String(value));
|
||||
}
|
||||
if (args && translationFromDictionary) {
|
||||
return Object.entries(args)
|
||||
.reduce(
|
||||
(acc, [key, value]) => acc.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), String(value)),
|
||||
String(translationFromDictionary),
|
||||
);
|
||||
}
|
||||
|
||||
return translation;
|
||||
return translationFromDictionary;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ describe('locales', () => {
|
||||
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
|
||||
/^webhooks\.events\.[a-z0-9]+\.[a-z0-9:]+.description$/, // webhooks.events.documents.document:created.description
|
||||
/^webhooks\.events\.[a-z0-9]+\.title$/, // webhooks.events.documents.title
|
||||
/^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
|
||||
|
||||
@@ -68,6 +68,8 @@ export type LocaleKeys =
|
||||
| 'auth.legal-links.description'
|
||||
| 'auth.legal-links.terms'
|
||||
| 'auth.legal-links.privacy'
|
||||
| 'auth.no-auth-provider.title'
|
||||
| 'auth.no-auth-provider.description'
|
||||
| 'user.settings.title'
|
||||
| 'user.settings.description'
|
||||
| 'user.settings.email.title'
|
||||
@@ -229,6 +231,8 @@ export type LocaleKeys =
|
||||
| 'documents.deleted.deleted-at'
|
||||
| 'documents.deleted.restoring'
|
||||
| 'documents.deleted.deleting'
|
||||
| 'documents.preview.unknown-file-type'
|
||||
| 'documents.preview.binary-file'
|
||||
| 'trash.delete-all.button'
|
||||
| 'trash.delete-all.confirm.title'
|
||||
| 'trash.delete-all.confirm.description'
|
||||
@@ -272,7 +276,6 @@ export type LocaleKeys =
|
||||
| 'tags.form.name.required'
|
||||
| 'tags.form.name.max-length'
|
||||
| 'tags.form.color.label'
|
||||
| 'tags.form.color.placeholder'
|
||||
| 'tags.form.color.required'
|
||||
| 'tags.form.color.invalid'
|
||||
| 'tags.form.description.label'
|
||||
@@ -437,8 +440,12 @@ export type LocaleKeys =
|
||||
| 'webhooks.delete.confirm.message'
|
||||
| 'webhooks.delete.confirm.confirm-button'
|
||||
| 'webhooks.delete.confirm.cancel-button'
|
||||
| 'webhooks.events.documents.title'
|
||||
| 'webhooks.events.documents.document:created.description'
|
||||
| 'webhooks.events.documents.document:deleted.description'
|
||||
| 'webhooks.events.documents.document:updated.description'
|
||||
| 'webhooks.events.documents.document:tag:added.description'
|
||||
| 'webhooks.events.documents.document:tag:removed.description'
|
||||
| 'layout.menu.home'
|
||||
| 'layout.menu.documents'
|
||||
| 'layout.menu.tags'
|
||||
@@ -477,6 +484,8 @@ export type LocaleKeys =
|
||||
| 'api-errors.user.organization_invitation_limit_reached'
|
||||
| 'api-errors.demo.not_available'
|
||||
| 'api-errors.tags.already_exists'
|
||||
| 'api-errors.internal.error'
|
||||
| 'api-errors.auth.invalid_origin'
|
||||
| 'not-found.title'
|
||||
| 'not-found.description'
|
||||
| 'not-found.back-to-home'
|
||||
@@ -484,4 +493,9 @@ export type LocaleKeys =
|
||||
| 'demo.popup.discord'
|
||||
| 'demo.popup.discord-link-label'
|
||||
| 'demo.popup.reset'
|
||||
| 'demo.popup.hide';
|
||||
| 'demo.popup.hide'
|
||||
| 'color-picker.hue'
|
||||
| 'color-picker.saturation'
|
||||
| 'color-picker.lightness'
|
||||
| 'color-picker.select-color'
|
||||
| 'color-picker.select-a-color';
|
||||
|
||||
@@ -17,16 +17,26 @@ import { Alert, AlertDescription } from '@/modules/ui/components/alert';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { Card } from '@/modules/ui/components/card';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/modules/ui/components/dialog';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/modules/ui/components/dropdown-menu';
|
||||
import { EmptyState } from '@/modules/ui/components/empty';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||
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 AllowedOriginsDialog: Component<{
|
||||
children: (props: DialogTriggerProps) => JSX.Element;
|
||||
intakeEmails: IntakeEmail;
|
||||
open?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
}> = (props) => {
|
||||
const [getAllowedOrigins, setAllowedOrigins] = createSignal(props.intakeEmails?.allowedOrigins || []);
|
||||
const { t } = useI18n();
|
||||
|
||||
const update = async () => {
|
||||
if (!props.intakeEmails) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateIntakeEmail({
|
||||
organizationId: props.intakeEmails.organizationId,
|
||||
intakeEmailId: props.intakeEmails.id,
|
||||
@@ -58,13 +68,29 @@ const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) =>
|
||||
});
|
||||
|
||||
async function invalidateQuery() {
|
||||
if (!props.intakeEmails) {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['organizations', props.intakeEmails.organizationId, 'intake-emails'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!props.intakeEmails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={isOpen => !isOpen && invalidateQuery()}>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
invalidateQuery();
|
||||
}
|
||||
props.onOpenChange?.(isOpen);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger as={props.children} />
|
||||
|
||||
<DialogContent>
|
||||
@@ -129,6 +155,8 @@ const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) =>
|
||||
export const IntakeEmailsPage: Component = () => {
|
||||
const { config } = useConfig();
|
||||
const { t, te } = useI18n();
|
||||
const [selectedIntakeEmail, setSelectedIntakeEmail] = createSignal<IntakeEmail | null>(null);
|
||||
const [openDropdownId, setOpenDropdownId] = createSignal<string | null>(null);
|
||||
|
||||
if (!config.intakeEmails.isEnabled) {
|
||||
return (
|
||||
@@ -225,6 +253,11 @@ export const IntakeEmailsPage: Component = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const openAllowedOriginsDialog = (intakeEmail: IntakeEmail) => {
|
||||
setOpenDropdownId(null);
|
||||
setSelectedIntakeEmail(intakeEmail);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="p-6 max-w-screen-md mx-auto mt-10">
|
||||
<h1 class="text-xl font-semibold">{t('intake-emails.title')}</h1>
|
||||
@@ -313,39 +346,46 @@ export const IntakeEmailsPage: Component = () => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => updateEmail({ intakeEmailId: intakeEmail.id, isEnabled: !intakeEmail.isEnabled })}
|
||||
<DropdownMenu
|
||||
open={openDropdownId() === intakeEmail.id}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpenDropdownId(isOpen ? intakeEmail.id : null);
|
||||
}}
|
||||
>
|
||||
<div class="i-tabler-power size-4 mr-2" />
|
||||
{intakeEmail.isEnabled ? t('intake-emails.actions.disable') : t('intake-emails.actions.enable')}
|
||||
</Button>
|
||||
|
||||
<AllowedOriginsDialog intakeEmails={intakeEmail}>
|
||||
{(props: DialogTriggerProps) => (
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label="Edit intake email"
|
||||
{...props}
|
||||
class="flex items-center gap-2 leading-none"
|
||||
<DropdownMenuTrigger as={Button} variant="outline" aria-label="More actions" size="icon">
|
||||
<div class="i-tabler-dots-vertical size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setOpenDropdownId(null);
|
||||
updateEmail({ intakeEmailId: intakeEmail.id, isEnabled: !intakeEmail.isEnabled });
|
||||
}}
|
||||
>
|
||||
<div class="i-tabler-edit size-4" />
|
||||
{t('intake-emails.actions.manage-origins')}
|
||||
</Button>
|
||||
)}
|
||||
</AllowedOriginsDialog>
|
||||
<div class="i-tabler-power size-4 mr-2" />
|
||||
{intakeEmail.isEnabled ? t('intake-emails.actions.disable') : t('intake-emails.actions.enable')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => deleteEmail({ intakeEmailId: intakeEmail.id })}
|
||||
aria-label="Delete intake email"
|
||||
class="text-red"
|
||||
>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
{t('intake-emails.actions.delete')}
|
||||
</Button>
|
||||
<DropdownMenuItem
|
||||
onClick={() => openAllowedOriginsDialog(intakeEmail)}
|
||||
>
|
||||
<div class="i-tabler-edit size-4 mr-2" />
|
||||
{t('intake-emails.actions.manage-origins')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setOpenDropdownId(null);
|
||||
deleteEmail({ intakeEmailId: intakeEmail.id });
|
||||
}}
|
||||
class="text-red"
|
||||
>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
{t('intake-emails.actions.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -355,6 +395,22 @@ export const IntakeEmailsPage: Component = () => {
|
||||
)}
|
||||
</Show>
|
||||
</Suspense>
|
||||
|
||||
<Show when={selectedIntakeEmail()}>
|
||||
{intakeEmail => (
|
||||
<AllowedOriginsDialog
|
||||
intakeEmails={intakeEmail()}
|
||||
open={true}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setSelectedIntakeEmail(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{() => <div />}
|
||||
</AllowedOriginsDialog>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getRgbChannelsFromHex } from './color-formats';
|
||||
|
||||
describe('color-formats', () => {
|
||||
describe('getRgbChannelsFromHex', () => {
|
||||
test('extracts the rgb channels values from a hex color', () => {
|
||||
expect(getRgbChannelsFromHex('#000000')).toEqual({ r: 0, g: 0, b: 0 });
|
||||
expect(getRgbChannelsFromHex('#FFFFFF')).toEqual({ r: 255, g: 255, b: 255 });
|
||||
expect(getRgbChannelsFromHex('#FF0000')).toEqual({ r: 255, g: 0, b: 0 });
|
||||
expect(getRgbChannelsFromHex('#00FF00')).toEqual({ r: 0, g: 255, b: 0 });
|
||||
expect(getRgbChannelsFromHex('#0000FF')).toEqual({ r: 0, g: 0, b: 255 });
|
||||
expect(getRgbChannelsFromHex('#0000FF')).toEqual({ r: 0, g: 0, b: 255 });
|
||||
});
|
||||
|
||||
test('is case insensitive', () => {
|
||||
expect(getRgbChannelsFromHex('#ff0000')).toEqual({ r: 255, g: 0, b: 0 });
|
||||
expect(getRgbChannelsFromHex('#00ff00')).toEqual({ r: 0, g: 255, b: 0 });
|
||||
expect(getRgbChannelsFromHex('#0000ff')).toEqual({ r: 0, g: 0, b: 255 });
|
||||
});
|
||||
|
||||
test('returns 0, 0, 0 for invalid colors', () => {
|
||||
expect(getRgbChannelsFromHex('lorem')).toEqual({ r: 0, g: 0, b: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getRgbChannelsFromHex(color: string) {
|
||||
const [r, g, b] = color.match(/^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i)?.slice(1).map(c => Number.parseInt(c, 16)) ?? [0, 0, 0];
|
||||
|
||||
return { r, g, b };
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getLuminance } from './luminance';
|
||||
|
||||
describe('luminance', () => {
|
||||
describe('getLuminance', () => {
|
||||
test(`the relative luminance of a color is the relative brightness of any point in a color space, normalized to 0 for darkest black and 1 for lightest white
|
||||
the formula is: 0.2126 * R + 0.7152 * G + 0.0722 * B
|
||||
where R, G, B are the red, green, and blue channels of the color, normalized to 0-1 and gamma corrected (sRGB):
|
||||
if the channel value is less than 0.03928, it is divided by 12.92, otherwise it is raised to the power of 2.4
|
||||
|
||||
Source: https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
`, () => {
|
||||
expect(getLuminance('#000000')).toBe(0);
|
||||
expect(getLuminance('#FFFFFF')).toBe(1);
|
||||
expect(getLuminance('#FF0000')).toBeCloseTo(0.2126, 4);
|
||||
expect(getLuminance('#00FF00')).toBeCloseTo(0.7152, 4);
|
||||
expect(getLuminance('#0000FF')).toBeCloseTo(0.0722, 4);
|
||||
});
|
||||
});
|
||||
});
|
||||
17
apps/papra-client/src/modules/shared/colors/luminance.ts
Normal file
17
apps/papra-client/src/modules/shared/colors/luminance.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getRgbChannelsFromHex } from './color-formats';
|
||||
|
||||
// https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
export function getLuminance(color: string) {
|
||||
const { r, g, b } = getRgbChannelsFromHex(color);
|
||||
|
||||
const toLinear = (channelValue: number) => {
|
||||
const normalized = channelValue / 255;
|
||||
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
|
||||
const R = toLinear(r);
|
||||
const G = toLinear(g);
|
||||
const B = toLinear(b);
|
||||
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
}
|
||||
@@ -2,28 +2,46 @@ import type { LocaleKeys } from '@/modules/i18n/locales.types';
|
||||
import { get } from 'lodash-es';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
|
||||
function codeToKey(code: string): LocaleKeys {
|
||||
// Better auth may returns different error codes like INVALID_ORIGIN, INVALID_CALLBACKURL when the origin is invalid
|
||||
// codes are here https://github.com/better-auth/better-auth/blob/canary/packages/better-auth/src/api/middlewares/origin-check.ts#L71 (in lower case)
|
||||
if (code.match(/^INVALID_[A-Z]+URL$/) || code === 'INVALID_ORIGIN') {
|
||||
return `api-errors.auth.invalid_origin`;
|
||||
}
|
||||
|
||||
return `api-errors.${code}` as LocaleKeys;
|
||||
}
|
||||
|
||||
export function useI18nApiErrors({ t = useI18n().t }: { t?: ReturnType<typeof useI18n>['t'] } = {}) {
|
||||
const getTranslationFromApiErrorCode = ({ code }: { code: string }) => {
|
||||
return t(`api-errors.${code}` as LocaleKeys);
|
||||
};
|
||||
const getDefaultErrorMessage = () => t('api-errors.default');
|
||||
|
||||
const getTranslationFromApiError = ({ error }: { error: unknown }) => {
|
||||
const code = get(error, 'data.error.code') ?? get(error, 'code');
|
||||
|
||||
if (!code) {
|
||||
return t('api-errors.default');
|
||||
const getErrorMessage = (args: { error: unknown } | { code: string }) => {
|
||||
if ('code' in args) {
|
||||
const { code } = args;
|
||||
return t(codeToKey(code)) ?? getDefaultErrorMessage();
|
||||
}
|
||||
|
||||
return getTranslationFromApiErrorCode({ code });
|
||||
if ('error' in args) {
|
||||
const { error } = args;
|
||||
const code = get(error, 'data.error.code') ?? get(error, 'code');
|
||||
const translation = code ? t(codeToKey(code)) : undefined;
|
||||
|
||||
if (translation) {
|
||||
return translation;
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && error && 'message' in error && typeof error.message === 'string') {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultErrorMessage();
|
||||
};
|
||||
|
||||
return {
|
||||
getErrorMessage: (args: { error: unknown } | { code: string }) => {
|
||||
if ('error' in args) {
|
||||
return getTranslationFromApiError({ error: args.error });
|
||||
}
|
||||
|
||||
return getTranslationFromApiErrorCode({ code: args.code });
|
||||
getErrorMessage,
|
||||
createI18nApiError: (args: { error: unknown } | { code: string }) => {
|
||||
return new Error(getErrorMessage(args));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DialogTriggerProps } from '@kobalte/core/dialog';
|
||||
import type { Component, JSX } from 'solid-js';
|
||||
import type { Tag as TagType } from '../tags.types';
|
||||
import { safely } from '@corentinth/chisels';
|
||||
import { getValues } from '@modular-forms/solid';
|
||||
import { getValues, setValue } from '@modular-forms/solid';
|
||||
import { A, useParams } from '@solidjs/router';
|
||||
import { useQuery } from '@tanstack/solid-query';
|
||||
import { createSignal, For, Show, Suspense } from 'solid-js';
|
||||
@@ -14,6 +14,7 @@ import { createForm } from '@/modules/shared/form/form';
|
||||
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||
import { queryClient } from '@/modules/shared/query/query-client';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { ColorSwatchPicker } from '@/modules/ui/components/color-swatch-picker';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/modules/ui/components/dialog';
|
||||
import { EmptyState } from '@/modules/ui/components/empty';
|
||||
import { createToast } from '@/modules/ui/components/sonner';
|
||||
@@ -23,6 +24,26 @@ import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/component
|
||||
import { Tag } from '../components/tag.component';
|
||||
import { createTag, deleteTag, fetchTags, updateTag } from '../tags.services';
|
||||
|
||||
// To keep, useful for generating swatches
|
||||
// function generateSwatches(count = 9, saturation = 100, lightness = 74) {
|
||||
// const colors = [];
|
||||
// for (let i = 0; i < count; i++) {
|
||||
// const hue = Math.round((78 + i * 360 / count) % 360);
|
||||
// const hsl = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||
// colors.push(parseColor(hsl).toString('hex').toUpperCase());
|
||||
// }
|
||||
// return colors;
|
||||
// }
|
||||
|
||||
const defaultColors = ['#D8FF75', '#7FFF7A', '#7AFFCE', '#7AD7FF', '#7A7FFF', '#CE7AFF', '#FF7AD7', '#FF7A7F', '#FFCE7A', '#FFFFFF'];
|
||||
|
||||
const TagColorPicker: Component<{
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
}> = (props) => {
|
||||
return <ColorSwatchPicker value={props.color} onChange={props.onChange} colors={defaultColors} />;
|
||||
};
|
||||
|
||||
const TagForm: Component<{
|
||||
onSubmit: (values: { name: string; color: string; description: string }) => Promise<void>;
|
||||
initialValues?: { name?: string; color?: string; description?: string | null };
|
||||
@@ -71,10 +92,10 @@ const TagForm: Component<{
|
||||
</Field>
|
||||
|
||||
<Field name="color">
|
||||
{(field, inputProps) => (
|
||||
{field => (
|
||||
<TextFieldRoot class="flex flex-col gap-1 mb-4">
|
||||
<TextFieldLabel for="color">{t('tags.form.color.label')}</TextFieldLabel>
|
||||
<TextField id="color" {...inputProps} autoFocus value={field.value} aria-invalid={Boolean(field.error)} placeholder={t('tags.form.color.placeholder')} />
|
||||
<TagColorPicker color={field.value ?? ''} onChange={color => setValue(form, 'color', color)} />
|
||||
{field.error && <div class="text-red-500 text-sm">{field.error}</div>}
|
||||
</TextFieldRoot>
|
||||
)}
|
||||
@@ -119,7 +140,7 @@ export const CreateTagModal: Component<{
|
||||
const onSubmit = async ({ name, color, description }: { name: string; color: string; description: string }) => {
|
||||
const [,error] = await safely(createTag({
|
||||
name,
|
||||
color,
|
||||
color: color.toLowerCase(),
|
||||
description,
|
||||
organizationId: props.organizationId,
|
||||
}));
|
||||
@@ -153,7 +174,7 @@ export const CreateTagModal: Component<{
|
||||
<DialogTitle>{t('tags.create')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<TagForm onSubmit={onSubmit} initialValues={{ color: '#d8ff75' }} />
|
||||
<TagForm onSubmit={onSubmit} initialValues={{ color: '#D8FF75' }} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -170,7 +191,7 @@ const UpdateTagModal: Component<{
|
||||
const onSubmit = async ({ name, color, description }: { name: string; color: string; description: string }) => {
|
||||
await updateTag({
|
||||
name,
|
||||
color,
|
||||
color: color.toLowerCase(),
|
||||
description,
|
||||
organizationId: props.organizationId,
|
||||
tagId: props.tag.id,
|
||||
@@ -207,6 +228,7 @@ export const TagsPage: Component = () => {
|
||||
const params = useParams();
|
||||
const { confirm } = useConfirmModal();
|
||||
const { t } = useI18n();
|
||||
const { getErrorMessage } = useI18nApiErrors({ t });
|
||||
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['organizations', params.organizationId, 'tags'],
|
||||
@@ -231,10 +253,19 @@ export const TagsPage: Component = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteTag({
|
||||
const [, error] = await safely(deleteTag({
|
||||
organizationId: params.organizationId,
|
||||
tagId: tag.id,
|
||||
});
|
||||
}));
|
||||
|
||||
if (error) {
|
||||
createToast({
|
||||
message: getErrorMessage({ error }),
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['organizations', params.organizationId],
|
||||
|
||||
@@ -24,8 +24,8 @@ export const alertVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type alertProps<T extends ValidComponent = 'div'> = AlertRootProps<T> &
|
||||
VariantProps<typeof alertVariants> & {
|
||||
type alertProps<T extends ValidComponent = 'div'> = AlertRootProps<T>
|
||||
& VariantProps<typeof alertVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ export const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type buttonProps<T extends ValidComponent = 'button'> = ButtonRootProps<T> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
type buttonProps<T extends ValidComponent = 'button'> = ButtonRootProps<T>
|
||||
& VariantProps<typeof buttonVariants> & {
|
||||
class?: string;
|
||||
isLoading?: boolean;
|
||||
children?: JSX.Element;
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import type { Color } from '@kobalte/core/colors';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import type { Component, ParentProps } from 'solid-js';
|
||||
import { ColorSlider } from '@kobalte/core/color-slider';
|
||||
import { parseColor } from '@kobalte/core/colors';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { createSignal, For, splitProps } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { getLuminance } from '@/modules/shared/colors/luminance';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
import { Button } from './button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||
import { TextField, TextFieldRoot } from './textfield';
|
||||
|
||||
const Slider: Component<{
|
||||
channel: 'hue' | 'saturation' | 'lightness';
|
||||
label: string;
|
||||
value: Color;
|
||||
onChange?: (value: Color) => void;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<ColorSlider channel={props.channel} class="relative flex flex-col gap-0.5 w-full" value={props.value} onChange={props.onChange}>
|
||||
<div class="flex items-center justify-between text-xs font-medium text-muted-foreground">
|
||||
<ColorSlider.Label>{props.label}</ColorSlider.Label>
|
||||
<ColorSlider.ValueLabel />
|
||||
</div>
|
||||
<ColorSlider.Track class="w-full h-24px rounded relative ">
|
||||
<ColorSlider.Thumb class="w-4 h-4 top-4px rounded-full bg-[var(--kb-color-current)] border-2 border-#0a0a0a">
|
||||
<ColorSlider.Input />
|
||||
</ColorSlider.Thumb>
|
||||
</ColorSlider.Track>
|
||||
</ColorSlider>
|
||||
);
|
||||
};
|
||||
|
||||
const ColorPicker: Component<{
|
||||
color: string;
|
||||
onChange?: (color: string) => void;
|
||||
}> = (props) => {
|
||||
const { t } = useI18n();
|
||||
const [color, setColor] = createSignal<Color>(parseColor(props.color).toFormat('hsl'));
|
||||
|
||||
const onUpdateColor = (color: Color) => {
|
||||
setColor(color.toFormat('hsl'));
|
||||
props.onChange?.(color.toString('hex').toUpperCase());
|
||||
};
|
||||
|
||||
const onInputColorChange = (e: Event) => {
|
||||
const color = (e.target as HTMLInputElement).value;
|
||||
|
||||
try {
|
||||
const parsedColor = parseColor(color);
|
||||
onUpdateColor(parsedColor);
|
||||
} catch (_error) {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<Slider channel="hue" label={t('color-picker.hue')} value={color()} onChange={onUpdateColor} />
|
||||
<Slider channel="saturation" label={t('color-picker.saturation')} value={color()} onChange={onUpdateColor} />
|
||||
<Slider channel="lightness" label={t('color-picker.lightness')} value={color()} onChange={onUpdateColor} />
|
||||
|
||||
<TextFieldRoot>
|
||||
<TextField value={color().toString('hex').toUpperCase()} onInput={onInputColorChange} placeholder="#000000" />
|
||||
</TextFieldRoot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const colorSwatchVariants = cva(
|
||||
'rounded-lg border-2 border-background shadow-sm transition-all hover:scale-110 focus-visible:(outline-none ring-1.5 ring-ring ring-offset-1)',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-6 w-6',
|
||||
md: 'h-8 w-8',
|
||||
lg: 'h-10 w-10',
|
||||
},
|
||||
selected: {
|
||||
true: 'ring-1.5 ring-primary! ring-offset-1',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
selected: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type ColorSwatchPickerProps = ParentProps<{
|
||||
value?: string;
|
||||
onChange?: (color: string) => void;
|
||||
colors?: string[];
|
||||
size?: VariantProps<typeof colorSwatchVariants>['size'];
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
}>;
|
||||
|
||||
export function ColorSwatchPicker(props: ColorSwatchPickerProps) {
|
||||
const { t } = useI18n();
|
||||
const [local, rest] = splitProps(props, [
|
||||
'value',
|
||||
'onChange',
|
||||
'colors',
|
||||
'size',
|
||||
'class',
|
||||
'disabled',
|
||||
'children',
|
||||
]);
|
||||
|
||||
const colors = () => local.colors ?? [];
|
||||
const selectedColor = () => local.value ?? colors()[0];
|
||||
|
||||
const handleColorSelect = (color: string) => {
|
||||
if (!local.disabled && local.onChange) {
|
||||
local.onChange(color);
|
||||
}
|
||||
};
|
||||
|
||||
const getIsNotInSwatch = (color?: string) => color && !colors().includes(color);
|
||||
|
||||
function getContrastTextColor(color: string) {
|
||||
const luminance = getLuminance(color);
|
||||
// 0.179 is the threshold for WCAG 2.0 level AA
|
||||
return luminance > 0.179 ? 'black' : 'white';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
'inline-flex items-center gap-1 flex-wrap',
|
||||
local.disabled && 'opacity-50 cursor-not-allowed',
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<For each={colors()}>
|
||||
{color => (
|
||||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
colorSwatchVariants({
|
||||
size: local.size,
|
||||
selected: selectedColor() === color,
|
||||
}),
|
||||
)}
|
||||
style={{ 'background-color': color }}
|
||||
onClick={() => handleColorSelect(color)}
|
||||
disabled={local.disabled}
|
||||
aria-label={`${t('color-picker.select-color')} ${color}`}
|
||||
title={color}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
as={Button}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class={cn(getIsNotInSwatch(local.value) && 'ring-1.5 ring-primary! ring-offset-1')}
|
||||
style={{ 'background-color': getIsNotInSwatch(local.value) ? local.value : '' }}
|
||||
aria-label={t('color-picker.select-a-color')}
|
||||
>
|
||||
<div class="i-tabler-plus size-4" style={{ color: getIsNotInSwatch(local.value) ? getContrastTextColor(local.value ?? '') : undefined }}></div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<p class="text-sm font-medium mb-4">{t('color-picker.select-a-color')}</p>
|
||||
|
||||
<ColorPicker color={local.value ?? ''} onChange={local?.onChange} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -88,8 +88,8 @@ export function ComboboxTrigger<T extends ValidComponent = 'button'>(props: Poly
|
||||
);
|
||||
}
|
||||
|
||||
type comboboxContentProps<T extends ValidComponent = 'div'> =
|
||||
ComboboxContentProps<T> & {
|
||||
type comboboxContentProps<T extends ValidComponent = 'div'>
|
||||
= ComboboxContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ export function DialogTitle<T extends ValidComponent = 'h2'>(props: PolymorphicP
|
||||
);
|
||||
}
|
||||
|
||||
type dialogDescriptionProps<T extends ValidComponent = 'p'> =
|
||||
DialogDescriptionProps<T> & {
|
||||
type dialogDescriptionProps<T extends ValidComponent = 'p'>
|
||||
= DialogDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ export function DropdownMenu(props: DropdownMenuRootProps) {
|
||||
return <DropdownMenuPrimitive {...merge} />;
|
||||
}
|
||||
|
||||
type dropdownMenuContentProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuContentProps<T> & {
|
||||
type dropdownMenuContentProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -49,8 +49,8 @@ export function DropdownMenuContent<T extends ValidComponent = 'div'>(props: Pol
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuItemProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuItemProps<T> & {
|
||||
type dropdownMenuItemProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuItemProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
@@ -73,8 +73,8 @@ export function DropdownMenuItem<T extends ValidComponent = 'div'>(props: Polymo
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'> =
|
||||
DropdownMenuGroupLabelProps<T> & {
|
||||
type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'>
|
||||
= DropdownMenuGroupLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -92,8 +92,8 @@ export function DropdownMenuGroupLabel<T extends ValidComponent = 'span'>(props:
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuItemLabelProps<T> & {
|
||||
type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuItemLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -111,8 +111,8 @@ export function DropdownMenuItemLabel<T extends ValidComponent = 'div'>(props: P
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'> =
|
||||
DropdownMenuSeparatorProps<T> & {
|
||||
type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'>
|
||||
= DropdownMenuSeparatorProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -178,8 +178,8 @@ export function DropdownMenuSubTrigger<T extends ValidComponent = 'div'>(props:
|
||||
);
|
||||
}
|
||||
|
||||
type dropdownMenuSubContentProps<T extends ValidComponent = 'div'> =
|
||||
DropdownMenuSubTriggerProps<T> & {
|
||||
type dropdownMenuSubContentProps<T extends ValidComponent = 'div'>
|
||||
= DropdownMenuSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ export function NumberFieldErrorMessage<T extends ValidComponent = 'div'>(props:
|
||||
);
|
||||
}
|
||||
|
||||
type numberFieldProps<T extends ValidComponent = 'div'> =
|
||||
NumberFieldRootProps<T> & {
|
||||
type numberFieldProps<T extends ValidComponent = 'div'>
|
||||
= NumberFieldRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -87,8 +87,8 @@ export function NumberFieldGroup(props: ComponentProps<'div'>) {
|
||||
);
|
||||
}
|
||||
|
||||
type numberFieldInputProps<T extends ValidComponent = 'input'> =
|
||||
NumberFieldInputProps<T> & {
|
||||
type numberFieldInputProps<T extends ValidComponent = 'input'>
|
||||
= NumberFieldInputProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ export function SelectTrigger<T extends ValidComponent = 'button'>(props: Polymo
|
||||
);
|
||||
}
|
||||
|
||||
type selectContentProps<T extends ValidComponent = 'div'> =
|
||||
SelectContentProps<T> & {
|
||||
type selectContentProps<T extends ValidComponent = 'div'>
|
||||
= SelectContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ export const sheetVariants = cva(
|
||||
);
|
||||
|
||||
type sheetContentProps<T extends ValidComponent = 'div'> = ParentProps<
|
||||
DialogContentProps<T> &
|
||||
VariantProps<typeof sheetVariants> & {
|
||||
DialogContentProps<T>
|
||||
& VariantProps<typeof sheetVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
@@ -96,8 +96,8 @@ export function SheetTitle<T extends ValidComponent = 'h2'>(props: PolymorphicPr
|
||||
);
|
||||
}
|
||||
|
||||
type sheetDescriptionProps<T extends ValidComponent = 'p'> =
|
||||
DialogDescriptionProps<T> & {
|
||||
type sheetDescriptionProps<T extends ValidComponent = 'p'>
|
||||
= DialogDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ export function TabsList<T extends ValidComponent = 'div'>(props: PolymorphicPro
|
||||
);
|
||||
}
|
||||
|
||||
type tabsContentProps<T extends ValidComponent = 'div'> =
|
||||
TabsContentProps<T> & {
|
||||
type tabsContentProps<T extends ValidComponent = 'div'>
|
||||
= TabsContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -65,8 +65,8 @@ export function TabsContent<T extends ValidComponent = 'div'>(props: Polymorphic
|
||||
);
|
||||
}
|
||||
|
||||
type tabsTriggerProps<T extends ValidComponent = 'button'> =
|
||||
TabsTriggerProps<T> & {
|
||||
type tabsTriggerProps<T extends ValidComponent = 'button'>
|
||||
= TabsTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -100,8 +100,8 @@ const tabsIndicatorVariants = cva(
|
||||
);
|
||||
|
||||
type tabsIndicatorProps<T extends ValidComponent = 'div'> = VoidProps<
|
||||
TabsIndicatorProps<T> &
|
||||
VariantProps<typeof tabsIndicatorVariants> & {
|
||||
TabsIndicatorProps<T>
|
||||
& VariantProps<typeof tabsIndicatorVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -12,8 +12,8 @@ import { cva } from 'class-variance-authority';
|
||||
import { splitProps } from 'solid-js';
|
||||
import { cn } from '@/modules/shared/style/cn';
|
||||
|
||||
type textFieldProps<T extends ValidComponent = 'div'> =
|
||||
TextFieldRootProps<T> & {
|
||||
type textFieldProps<T extends ValidComponent = 'div'>
|
||||
= TextFieldRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -43,8 +43,8 @@ export const textfieldLabel = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type textFieldLabelProps<T extends ValidComponent = 'label'> =
|
||||
TextFieldLabelProps<T> & {
|
||||
type textFieldLabelProps<T extends ValidComponent = 'label'>
|
||||
= TextFieldLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -59,8 +59,8 @@ export function TextFieldLabel<T extends ValidComponent = 'label'>(props: Polymo
|
||||
);
|
||||
}
|
||||
|
||||
type textFieldErrorMessageProps<T extends ValidComponent = 'div'> =
|
||||
TextFieldErrorMessageProps<T> & {
|
||||
type textFieldErrorMessageProps<T extends ValidComponent = 'div'>
|
||||
= TextFieldErrorMessageProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
@@ -77,8 +77,8 @@ export function TextFieldErrorMessage<T extends ValidComponent = 'div'>(props: P
|
||||
);
|
||||
}
|
||||
|
||||
type textFieldDescriptionProps<T extends ValidComponent = 'div'> =
|
||||
TextFieldDescriptionProps<T> & {
|
||||
type textFieldDescriptionProps<T extends ValidComponent = 'div'>
|
||||
= TextFieldDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ function useToggleGroup() {
|
||||
}
|
||||
|
||||
type toggleGroupProps<T extends ValidComponent = 'div'> = ParentProps<
|
||||
ToggleGroupRootProps<T> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
ToggleGroupRootProps<T>
|
||||
& VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
@@ -56,8 +56,8 @@ export function ToggleGroup<T extends ValidComponent = 'div'>(props: Polymorphic
|
||||
);
|
||||
}
|
||||
|
||||
type toggleGroupItemProps<T extends ValidComponent = 'button'> =
|
||||
ToggleGroupItemProps<T> & {
|
||||
type toggleGroupItemProps<T extends ValidComponent = 'button'>
|
||||
= ToggleGroupItemProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ export const toggleVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type toggleButtonProps<T extends ValidComponent = 'button'> =
|
||||
ToggleButtonRootProps<T> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
type toggleButtonProps<T extends ValidComponent = 'button'>
|
||||
= ToggleButtonRootProps<T>
|
||||
& VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export function ToggleButton<T extends ValidComponent = 'button'>(props: PolymorphicProps<T, toggleButtonProps<T>>) {
|
||||
const [local, rest] = splitProps(props as toggleButtonProps, [
|
||||
|
||||
@@ -22,8 +22,8 @@ export function Tooltip(props: TooltipRootProps) {
|
||||
return <TooltipPrimitive {...merge} />;
|
||||
}
|
||||
|
||||
type tooltipContentProps<T extends ValidComponent = 'div'> =
|
||||
TooltipContentProps<T> & {
|
||||
type tooltipContentProps<T extends ValidComponent = 'div'>
|
||||
= TooltipContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export const OrganizationSettingsLayout: ParentComponent = (props) => {
|
||||
mainMenu={getNavigationItems()}
|
||||
header={() => (
|
||||
<div class="pl-6 py-3 border-b border-b-border flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="text-muted-foreground" as={A} href="/">
|
||||
<Button variant="ghost" size="icon" class="text-muted-foreground" as={A} href={`/organizations/${params.organizationId}`}>
|
||||
<div class="i-tabler-arrow-left size-5"></div>
|
||||
</Button>
|
||||
<h1 class="text-base font-bold">
|
||||
|
||||
@@ -46,7 +46,8 @@ export const WebhookEventsPicker: Component<{ events: WebhookEvent[]; onChange:
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
{/* <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> */}
|
||||
<For each={getEventsSections()}>
|
||||
{section => (
|
||||
<div>
|
||||
|
||||
@@ -4,6 +4,9 @@ export const WEBHOOK_EVENTS = [
|
||||
events: [
|
||||
'document:created',
|
||||
'document:deleted',
|
||||
'document:updated',
|
||||
'document:tag:added',
|
||||
'document:tag:removed',
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { cwd as getCwd } from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse } from 'yaml';
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
|
||||
export async function generateI18nTypes({ cwd = getCwd() }: { cwd?: string } = {}) {
|
||||
try {
|
||||
const yamlPath = path.join(cwd, 'src/locales/en.yml');
|
||||
@@ -17,7 +20,7 @@ export async function generateI18nTypes({ cwd = getCwd() }: { cwd?: string } = {
|
||||
// Do not manually edit this file.
|
||||
// This file is dynamically generated when the dev server runs (or using the \`pnpm script:generate-i18n-types\` command).
|
||||
// Keys are extracted from the en.yml file.
|
||||
// Source code : ${path.relative(cwd, __filename)}
|
||||
// Source code : ${path.relative(cwd, filename)}
|
||||
|
||||
export type LocaleKeys =\n${localKeys.map(key => ` | '${key}'`).join('\n')};
|
||||
`.trimStart();
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
# @papra/app-server
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#452](https://github.com/papra-hq/papra/pull/452) [`7f7e5bf`](https://github.com/papra-hq/papra/commit/7f7e5bffcbcfb843f3b2458400dfb44409a44867) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Completely rewrote the migration mechanism
|
||||
|
||||
- [#447](https://github.com/papra-hq/papra/pull/447) [`b5ccc13`](https://github.com/papra-hq/papra/commit/b5ccc135ba7f4359eaf85221bcb40ee63ba7d6c7) Thanks [@CorentinTh](https://github.com/CorentinTh)! - The file content extraction (like OCR) is now done asynchronously by the task runner
|
||||
|
||||
- [#448](https://github.com/papra-hq/papra/pull/448) [`5868800`](https://github.com/papra-hq/papra/commit/5868800bcec6ed69b5441b50e4445fae5cdb5bfb) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fixed the impossibility to delete a tag that has been assigned to a document
|
||||
|
||||
- [#432](https://github.com/papra-hq/papra/pull/432) [`6723baf`](https://github.com/papra-hq/papra/commit/6723baf98ad46f989fe1e1e19ad0dd25622cca77) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added new webhook events: document:updated, document:tag:added, document:tag:removed
|
||||
|
||||
- [#432](https://github.com/papra-hq/papra/pull/432) [`6723baf`](https://github.com/papra-hq/papra/commit/6723baf98ad46f989fe1e1e19ad0dd25622cca77) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Webhooks invocation is now defered
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#455](https://github.com/papra-hq/papra/pull/455) [`b33fde3`](https://github.com/papra-hq/papra/commit/b33fde35d3e8622e31b51aadfe56875d8e48a2ef) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Improved feedback message in case of invalid origin configuration
|
||||
|
||||
- Updated dependencies [[`a8cff8c`](https://github.com/papra-hq/papra/commit/a8cff8cedc062be3ed1d454e9de6e456553a4d8c), [`6723baf`](https://github.com/papra-hq/papra/commit/6723baf98ad46f989fe1e1e19ad0dd25622cca77), [`6723baf`](https://github.com/papra-hq/papra/commit/6723baf98ad46f989fe1e1e19ad0dd25622cca77), [`67b3b14`](https://github.com/papra-hq/papra/commit/67b3b14cdfa994874c695b9d854a93160ba6a911)]:
|
||||
- @papra/webhooks@0.2.0
|
||||
- @papra/lecture@0.1.0
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#417](https://github.com/papra-hq/papra/pull/417) [`a82ff3a`](https://github.com/papra-hq/papra/commit/a82ff3a755fa1164b4d8ff09b591ed6482af0ccc) Thanks [@CorentinTh](https://github.com/CorentinTh)! - v0.7 release
|
||||
|
||||
## 0.6.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#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
|
||||
|
||||
- [#392](https://github.com/papra-hq/papra/pull/392) [`21a5ccc`](https://github.com/papra-hq/papra/commit/21a5ccce6d42fde143fd3596918dfdfc9af577a1) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix permission issue for non 1000:1000 rootless user
|
||||
|
||||
- [#387](https://github.com/papra-hq/papra/pull/387) [`73b8d08`](https://github.com/papra-hq/papra/commit/73b8d080765b6eb9b479db39740cdc6972f6585d) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added configuration for the ocr language using DOCUMENTS_OCR_LANGUAGES
|
||||
|
||||
- [#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
|
||||
|
||||
- Updated dependencies [[`6cedc30`](https://github.com/papra-hq/papra/commit/6cedc30716e320946f79a0a9fd8d3b26e834f4db)]:
|
||||
- @papra/webhooks@0.1.1
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defineConfig } from 'drizzle-kit';
|
||||
export default defineConfig({
|
||||
schema: ['./src/modules/**/*.table.ts', './src/modules/**/*.tables.ts'],
|
||||
dialect: 'turso',
|
||||
out: './migrations',
|
||||
out: './src/migrations',
|
||||
dbCredentials: {
|
||||
url: env.DATABASE_URL ?? 'file:./db.sqlite',
|
||||
authToken: env.DATABASE_AUTH_TOKEN,
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu({
|
||||
typescript: {
|
||||
tsconfigPath: './tsconfig.json',
|
||||
overridesTypeAware: {
|
||||
'ts/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||||
'ts/strict-boolean-expressions': ['error', { allowNullableObject: true }],
|
||||
},
|
||||
|
||||
},
|
||||
stylistic: {
|
||||
semi: true,
|
||||
},
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
CREATE TABLE `documents` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`is_deleted` integer DEFAULT false NOT NULL,
|
||||
`deleted_at` integer,
|
||||
`organization_id` text NOT NULL,
|
||||
`created_by` text,
|
||||
`deleted_by` text,
|
||||
`original_name` text NOT NULL,
|
||||
`original_size` integer DEFAULT 0 NOT NULL,
|
||||
`original_storage_key` text NOT NULL,
|
||||
`original_sha256_hash` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`mime_type` text NOT NULL,
|
||||
`content` text DEFAULT '' NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE set null,
|
||||
FOREIGN KEY (`deleted_by`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `documents_organization_id_is_deleted_created_at_index` ON `documents` (`organization_id`,`is_deleted`,`created_at`);--> statement-breakpoint
|
||||
CREATE INDEX `documents_organization_id_is_deleted_index` ON `documents` (`organization_id`,`is_deleted`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `documents_organization_id_original_sha256_hash_unique` ON `documents` (`organization_id`,`original_sha256_hash`);--> statement-breakpoint
|
||||
CREATE INDEX `documents_original_sha256_hash_index` ON `documents` (`original_sha256_hash`);--> statement-breakpoint
|
||||
CREATE INDEX `documents_organization_id_size_index` ON `documents` (`organization_id`,`original_size`);--> statement-breakpoint
|
||||
CREATE TABLE `organization_invitations` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`role` text,
|
||||
`status` text NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`inviter_id` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`inviter_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `organization_members` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `organization_members_user_organization_unique` ON `organization_members` (`organization_id`,`user_id`);--> statement-breakpoint
|
||||
CREATE TABLE `organizations` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`customer_id` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_roles` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `user_roles_role_index` ON `user_roles` (`role`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_roles_user_id_role_unique_index` ON `user_roles` (`user_id`,`role`);--> statement-breakpoint
|
||||
CREATE TABLE `documents_tags` (
|
||||
`document_id` text NOT NULL,
|
||||
`tag_id` text NOT NULL,
|
||||
PRIMARY KEY(`document_id`, `tag_id`),
|
||||
FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `tags` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`color` text NOT NULL,
|
||||
`description` text,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `tags_organization_id_name_unique` ON `tags` (`organization_id`,`name`);--> statement-breakpoint
|
||||
CREATE TABLE `users` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`email_verified` integer DEFAULT false NOT NULL,
|
||||
`name` text,
|
||||
`image` text,
|
||||
`max_organization_count` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
|
||||
CREATE INDEX `users_email_index` ON `users` (`email`);--> statement-breakpoint
|
||||
CREATE TABLE `auth_accounts` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`user_id` text,
|
||||
`account_id` text NOT NULL,
|
||||
`provider_id` text NOT NULL,
|
||||
`access_token` text,
|
||||
`refresh_token` text,
|
||||
`access_token_expires_at` integer,
|
||||
`refresh_token_expires_at` integer,
|
||||
`scope` text,
|
||||
`id_token` text,
|
||||
`password` text,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `auth_sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`token` text NOT NULL,
|
||||
`user_id` text,
|
||||
`expires_at` integer NOT NULL,
|
||||
`ip_address` text,
|
||||
`user_agent` text,
|
||||
`active_organization_id` text,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`active_organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `auth_sessions_token_index` ON `auth_sessions` (`token`);--> statement-breakpoint
|
||||
CREATE TABLE `auth_verifications` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`identifier` text NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`expires_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `auth_verifications_identifier_index` ON `auth_verifications` (`identifier`);--> statement-breakpoint
|
||||
CREATE TABLE `intake_emails` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`email_address` text NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`allowed_origins` text DEFAULT '[]' NOT NULL,
|
||||
`is_enabled` integer DEFAULT true NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `intake_emails_email_address_unique` ON `intake_emails` (`email_address`);--> statement-breakpoint
|
||||
CREATE TABLE `organization_subscriptions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`customer_id` text NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`plan_id` text NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`seats_count` integer NOT NULL,
|
||||
`current_period_end` integer NOT NULL,
|
||||
`current_period_start` integer NOT NULL,
|
||||
`cancel_at_period_end` integer DEFAULT false NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
@@ -1,23 +0,0 @@
|
||||
-- Migration for adding full-text search virtual table for documents
|
||||
|
||||
CREATE VIRTUAL TABLE documents_fts USING fts5(id UNINDEXED, name, original_name, content, prefix='2 3 4');
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Copy data from documents to documents_fts for existing records
|
||||
INSERT INTO documents_fts(id, name, original_name, content)
|
||||
SELECT id, name, original_name, content FROM documents;
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TRIGGER trigger_documents_fts_insert AFTER INSERT ON documents BEGIN
|
||||
INSERT INTO documents_fts(id, name, original_name, content) VALUES (new.id, new.name, new.original_name, new.content);
|
||||
END;
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TRIGGER trigger_documents_fts_update AFTER UPDATE ON documents BEGIN
|
||||
UPDATE documents_fts SET name = new.name, original_name = new.original_name, content = new.content WHERE id = new.id;
|
||||
END;
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TRIGGER trigger_documents_fts_delete AFTER DELETE ON documents BEGIN
|
||||
DELETE FROM documents_fts WHERE id = old.id;
|
||||
END;
|
||||
@@ -1,32 +0,0 @@
|
||||
CREATE TABLE `tagging_rule_actions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`tagging_rule_id` text NOT NULL,
|
||||
`tag_id` text NOT NULL,
|
||||
FOREIGN KEY (`tagging_rule_id`) REFERENCES `tagging_rules`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `tagging_rule_conditions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`tagging_rule_id` text NOT NULL,
|
||||
`field` text NOT NULL,
|
||||
`operator` text NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`is_case_sensitive` integer DEFAULT false NOT NULL,
|
||||
FOREIGN KEY (`tagging_rule_id`) REFERENCES `tagging_rules`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `tagging_rules` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
CREATE TABLE `api_key_organizations` (
|
||||
`api_key_id` text NOT NULL,
|
||||
`organization_member_id` text NOT NULL,
|
||||
FOREIGN KEY (`api_key_id`) REFERENCES `api_keys`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`organization_member_id`) REFERENCES `organization_members`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `api_keys` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`key_hash` text NOT NULL,
|
||||
`prefix` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`last_used_at` integer,
|
||||
`expires_at` integer,
|
||||
`permissions` text DEFAULT '[]' NOT NULL,
|
||||
`all_organizations` integer DEFAULT false NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `api_keys_key_hash_unique` ON `api_keys` (`key_hash`);--> statement-breakpoint
|
||||
CREATE INDEX `key_hash_index` ON `api_keys` (`key_hash`);
|
||||
@@ -1,35 +0,0 @@
|
||||
CREATE TABLE `webhook_deliveries` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`webhook_id` text NOT NULL,
|
||||
`event_name` text NOT NULL,
|
||||
`request_payload` text NOT NULL,
|
||||
`response_payload` text NOT NULL,
|
||||
`response_status` integer NOT NULL,
|
||||
FOREIGN KEY (`webhook_id`) REFERENCES `webhooks`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `webhook_events` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`webhook_id` text NOT NULL,
|
||||
`event_name` text NOT NULL,
|
||||
FOREIGN KEY (`webhook_id`) REFERENCES `webhooks`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `webhook_events_webhook_id_event_name_unique` ON `webhook_events` (`webhook_id`,`event_name`);--> statement-breakpoint
|
||||
CREATE TABLE `webhooks` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`secret` text,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
`created_by` text,
|
||||
`organization_id` text,
|
||||
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE set null,
|
||||
FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
ALTER TABLE `organization_invitations` ALTER COLUMN "role" TO "role" text NOT NULL;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `organization_invitations_organization_email_unique` ON `organization_invitations` (`organization_id`,`email`);--> statement-breakpoint
|
||||
ALTER TABLE `organization_invitations` ALTER COLUMN "status" TO "status" text NOT NULL DEFAULT 'pending';
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE `document_activity_log` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`document_id` text NOT NULL,
|
||||
`event` text NOT NULL,
|
||||
`event_data` text,
|
||||
`user_id` text,
|
||||
`tag_id` text,
|
||||
FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE no action,
|
||||
FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE cascade ON DELETE no action
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@papra/app-server",
|
||||
"type": "module",
|
||||
"version": "0.6.3",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.9.0",
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"description": "Papra app server",
|
||||
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -18,8 +18,9 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"migrate:up": "tsx --env-file-if-exists=.env src/scripts/migrate-up.script.ts",
|
||||
"migrate:up": "tsx --env-file-if-exists=.env src/scripts/migrate-up.script.ts | crowlog-pretty",
|
||||
"migrate:push": "drizzle-kit push",
|
||||
"migrate:create": "sh -c 'drizzle-kit generate --name \"$1\" && tsx --env-file-if-exists=.env src/scripts/create-migration.ts \"$1\" | crowlog-pretty' --",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"clean:dist": "rm -rf dist",
|
||||
"clean:db": "rm db.sqlite",
|
||||
@@ -30,21 +31,23 @@
|
||||
"stripe:webhook": "stripe listen --forward-to localhost:1221/api/stripe/webhook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.817.0",
|
||||
"@aws-sdk/lib-storage": "^3.817.0",
|
||||
"@aws-sdk/client-s3": "^3.835.0",
|
||||
"@aws-sdk/lib-storage": "^3.835.0",
|
||||
"@azure/storage-blob": "^12.27.0",
|
||||
"@cadence-mq/core": "^0.2.1",
|
||||
"@cadence-mq/driver-memory": "^0.2.0",
|
||||
"@corentinth/chisels": "^1.3.1",
|
||||
"@corentinth/friendly-ids": "^0.0.1",
|
||||
"@crowlog/async-context-plugin": "^1.2.1",
|
||||
"@crowlog/logger": "^1.2.1",
|
||||
"@hono/node-server": "^1.14.3",
|
||||
"@hono/node-server": "^1.14.4",
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@owlrelay/api-sdk": "^0.0.2",
|
||||
"@owlrelay/webhook": "^0.0.3",
|
||||
"@papra/lecture": "^0.0.4",
|
||||
"@papra/lecture": "workspace:*",
|
||||
"@papra/webhooks": "workspace:*",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"backblaze-b2": "^1.7.0",
|
||||
"backblaze-b2": "^1.7.1",
|
||||
"better-auth": "catalog:",
|
||||
"c12": "^3.0.4",
|
||||
"chokidar": "^4.0.3",
|
||||
@@ -52,7 +55,7 @@
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"drizzle-orm": "^0.38.4",
|
||||
"figue": "^2.2.3",
|
||||
"hono": "^4.7.10",
|
||||
"hono": "^4.8.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime-types": "^3.0.1",
|
||||
"nanoid": "^5.1.5",
|
||||
@@ -61,12 +64,12 @@
|
||||
"p-limit": "^6.2.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"picomatch": "^4.0.2",
|
||||
"posthog-node": "^4.17.2",
|
||||
"resend": "^4.5.1",
|
||||
"posthog-node": "^4.18.0",
|
||||
"resend": "^4.6.0",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"stripe": "^17.7.0",
|
||||
"tsx": "^4.19.4",
|
||||
"zod": "^3.25.28"
|
||||
"tsx": "^4.20.3",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "catalog:",
|
||||
@@ -83,6 +86,7 @@
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "catalog:",
|
||||
"magicast": "^0.3.5",
|
||||
"memfs": "^4.17.2",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
|
||||
@@ -7,8 +7,8 @@ import { createServer } from './modules/app/server';
|
||||
import { parseConfig } from './modules/config/config';
|
||||
import { createIngestionFolderWatcher } from './modules/ingestion-folders/ingestion-folders.usecases';
|
||||
import { createLogger } from './modules/shared/logger/logger';
|
||||
import { createTaskScheduler } from './modules/tasks/task-scheduler';
|
||||
import { taskDefinitions } from './modules/tasks/tasks.defiitions';
|
||||
import { registerTaskDefinitions } from './modules/tasks/tasks.definitions';
|
||||
import { createTaskServices } from './modules/tasks/tasks.services';
|
||||
|
||||
const logger = createLogger({ namespace: 'app-server' });
|
||||
|
||||
@@ -17,8 +17,8 @@ const { config } = await parseConfig({ env });
|
||||
await ensureLocalDatabaseDirectoryExists({ config });
|
||||
const { db, client } = setupDatabase(config.database);
|
||||
|
||||
const { app } = await createServer({ config, db });
|
||||
const { taskScheduler } = createTaskScheduler({ config, taskDefinitions, tasksArgs: { db } });
|
||||
const taskServices = createTaskServices({ config });
|
||||
const { app } = await createServer({ config, db, taskServices });
|
||||
|
||||
const server = serve(
|
||||
{
|
||||
@@ -30,6 +30,7 @@ const server = serve(
|
||||
|
||||
if (config.ingestionFolder.isEnabled) {
|
||||
const { startWatchingIngestionFolders } = createIngestionFolderWatcher({
|
||||
taskServices,
|
||||
config,
|
||||
db,
|
||||
});
|
||||
@@ -37,11 +38,12 @@ if (config.ingestionFolder.isEnabled) {
|
||||
await startWatchingIngestionFolders();
|
||||
}
|
||||
|
||||
taskScheduler.start();
|
||||
await registerTaskDefinitions({ taskServices, db, config });
|
||||
|
||||
taskServices.start();
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
server.close();
|
||||
taskScheduler.stop();
|
||||
client.close();
|
||||
|
||||
process.exit(0);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { setupDatabase } from '../../modules/app/database/database';
|
||||
import { initialSchemaSetupMigration } from './0001-initial-schema-setup.migration';
|
||||
|
||||
describe('0001-initial-schema-setup migration', () => {
|
||||
describe('initialSchemaSetupMigration', () => {
|
||||
test('the up setup some default tables', async () => {
|
||||
const { db } = setupDatabase({ url: ':memory:' });
|
||||
await initialSchemaSetupMigration.up({ db });
|
||||
|
||||
const { rows: existingTables } = await db.run(sql`SELECT name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'`);
|
||||
|
||||
expect(existingTables.map(({ name }) => name)).to.eql([
|
||||
'documents',
|
||||
'documents_organization_id_is_deleted_created_at_index',
|
||||
'documents_organization_id_is_deleted_index',
|
||||
'documents_organization_id_original_sha256_hash_unique',
|
||||
'documents_original_sha256_hash_index',
|
||||
'documents_organization_id_size_index',
|
||||
'organization_invitations',
|
||||
'organization_members',
|
||||
'organization_members_user_organization_unique',
|
||||
'organizations',
|
||||
'user_roles',
|
||||
'user_roles_role_index',
|
||||
'user_roles_user_id_role_unique_index',
|
||||
'documents_tags',
|
||||
'tags',
|
||||
'tags_organization_id_name_unique',
|
||||
'users',
|
||||
'users_email_unique',
|
||||
'users_email_index',
|
||||
'auth_accounts',
|
||||
'auth_sessions',
|
||||
'auth_sessions_token_index',
|
||||
'auth_verifications',
|
||||
'auth_verifications_identifier_index',
|
||||
'intake_emails',
|
||||
'intake_emails_email_address_unique',
|
||||
'organization_subscriptions',
|
||||
]);
|
||||
|
||||
await initialSchemaSetupMigration.down({ db });
|
||||
|
||||
const { rows: existingTablesAfterDown } = await db.run(sql`SELECT name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'`);
|
||||
|
||||
expect(existingTablesAfterDown.map(({ name }) => name)).to.eql([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const initialSchemaSetupMigration = {
|
||||
name: 'initial-schema-setup',
|
||||
description: 'Creation of the base tables for the application',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "documents" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"is_deleted" integer DEFAULT false NOT NULL,
|
||||
"deleted_at" integer,
|
||||
"organization_id" text NOT NULL,
|
||||
"created_by" text,
|
||||
"deleted_by" text,
|
||||
"original_name" text NOT NULL,
|
||||
"original_size" integer DEFAULT 0 NOT NULL,
|
||||
"original_storage_key" text NOT NULL,
|
||||
"original_sha256_hash" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"mime_type" text NOT NULL,
|
||||
"content" text DEFAULT '' NOT NULL,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("created_by") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null,
|
||||
FOREIGN KEY ("deleted_by") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null
|
||||
);
|
||||
`),
|
||||
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "documents_organization_id_is_deleted_created_at_index" ON "documents" ("organization_id","is_deleted","created_at");`),
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "documents_organization_id_is_deleted_index" ON "documents" ("organization_id","is_deleted");`),
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "documents_organization_id_original_sha256_hash_unique" ON "documents" ("organization_id","original_sha256_hash");`),
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "documents_original_sha256_hash_index" ON "documents" ("original_sha256_hash");`),
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "documents_organization_id_size_index" ON "documents" ("organization_id","original_size");`),
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "organization_invitations" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"role" text,
|
||||
"status" text NOT NULL,
|
||||
"expires_at" integer NOT NULL,
|
||||
"inviter_id" text NOT NULL,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("inviter_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "organization_members" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"role" text NOT NULL,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "organization_members_user_organization_unique" ON "organization_members" ("organization_id","user_id");`),
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "organizations" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"customer_id" text
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "user_roles" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"role" text NOT NULL,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "user_roles_role_index" ON "user_roles" ("role");`),
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "user_roles_user_id_role_unique_index" ON "user_roles" ("user_id","role");`),
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "documents_tags" (
|
||||
"document_id" text NOT NULL,
|
||||
"tag_id" text NOT NULL,
|
||||
PRIMARY KEY("document_id", "tag_id"),
|
||||
FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "tags" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"color" text NOT NULL,
|
||||
"description" text,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "tags_organization_id_name_unique" ON "tags" ("organization_id","name");`),
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" integer DEFAULT false NOT NULL,
|
||||
"name" text,
|
||||
"image" text,
|
||||
"max_organization_count" integer
|
||||
);
|
||||
`),
|
||||
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "users_email_unique" ON "users" ("email");`),
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "users_email_index" ON "users" ("email");`),
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "auth_accounts" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"user_id" text,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"access_token_expires_at" integer,
|
||||
"refresh_token_expires_at" integer,
|
||||
"scope" text,
|
||||
"id_token" text,
|
||||
"password" text,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "auth_sessions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"user_id" text,
|
||||
"expires_at" integer NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"active_organization_id" text,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("active_organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE set null
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "auth_sessions_token_index" ON "auth_sessions" ("token");`),
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "auth_verifications" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expires_at" integer NOT NULL
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "auth_verifications_identifier_index" ON "auth_verifications" ("identifier");`),
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "intake_emails" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"email_address" text NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"allowed_origins" text DEFAULT '[]' NOT NULL,
|
||||
"is_enabled" integer DEFAULT true NOT NULL,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);`),
|
||||
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "intake_emails_email_address_unique" ON "intake_emails" ("email_address");`),
|
||||
db.run(sql`CREATE TABLE IF NOT EXISTS "organization_subscriptions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"customer_id" text NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"plan_id" text NOT NULL,
|
||||
"status" text NOT NULL,
|
||||
"seats_count" integer NOT NULL,
|
||||
"current_period_end" integer NOT NULL,
|
||||
"current_period_start" integer NOT NULL,
|
||||
"cancel_at_period_end" integer DEFAULT false NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);`),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
// Tables
|
||||
db.run(sql`DROP TABLE IF EXISTS "organization_subscriptions";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "intake_emails";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "auth_verifications";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "auth_sessions";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "auth_accounts";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "tags";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "documents_tags";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "user_roles";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "organizations";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "organization_members";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "organization_invitations";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "documents";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "users";`),
|
||||
|
||||
// // Indexes
|
||||
db.run(sql`DROP INDEX IF EXISTS "documents_organization_id_is_deleted_created_at_index";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "documents_organization_id_is_deleted_index";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "documents_organization_id_original_sha256_hash_unique";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "documents_original_sha256_hash_index";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "documents_organization_id_size_index";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "user_roles_role_index";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "user_roles_user_id_role_unique_index";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "tags_organization_id_name_unique";`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "users_email_unique";`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const documentsFtsMigration = {
|
||||
name: 'documents-fts',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(id UNINDEXED, name, original_name, content, prefix='2 3 4')`),
|
||||
db.run(sql`INSERT INTO documents_fts(id, name, original_name, content) SELECT id, name, original_name, content FROM documents`),
|
||||
db.run(sql`
|
||||
CREATE TRIGGER IF NOT EXISTS trigger_documents_fts_insert AFTER INSERT ON documents BEGIN
|
||||
INSERT INTO documents_fts(id, name, original_name, content) VALUES (new.id, new.name, new.original_name, new.content);
|
||||
END
|
||||
`),
|
||||
db.run(sql`
|
||||
CREATE TRIGGER IF NOT EXISTS trigger_documents_fts_update AFTER UPDATE ON documents BEGIN
|
||||
UPDATE documents_fts SET name = new.name, original_name = new.original_name, content = new.content WHERE id = new.id;
|
||||
END
|
||||
`),
|
||||
db.run(sql`
|
||||
CREATE TRIGGER IF NOT EXISTS trigger_documents_fts_delete AFTER DELETE ON documents BEGIN
|
||||
DELETE FROM documents_fts WHERE id = old.id;
|
||||
END
|
||||
`),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`DROP TRIGGER IF EXISTS trigger_documents_fts_insert`),
|
||||
db.run(sql`DROP TRIGGER IF EXISTS trigger_documents_fts_update`),
|
||||
db.run(sql`DROP TRIGGER IF EXISTS trigger_documents_fts_delete`),
|
||||
db.run(sql`DROP TABLE IF EXISTS documents_fts`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const taggingRulesMigration = {
|
||||
name: 'tagging-rules',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "tagging_rule_actions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"tagging_rule_id" text NOT NULL,
|
||||
"tag_id" text NOT NULL,
|
||||
FOREIGN KEY ("tagging_rule_id") REFERENCES "tagging_rules"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "tagging_rule_conditions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"tagging_rule_id" text NOT NULL,
|
||||
"field" text NOT NULL,
|
||||
"operator" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"is_case_sensitive" integer DEFAULT false NOT NULL,
|
||||
FOREIGN KEY ("tagging_rule_id") REFERENCES "tagging_rules"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "tagging_rules" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"enabled" integer DEFAULT true NOT NULL,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`DROP TABLE IF EXISTS "tagging_rule_actions"`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "tagging_rule_conditions"`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "tagging_rules"`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const apiKeysMigration = {
|
||||
name: 'api-keys',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "api_key_organizations" (
|
||||
"api_key_id" text NOT NULL,
|
||||
"organization_member_id" text NOT NULL,
|
||||
FOREIGN KEY ("api_key_id") REFERENCES "api_keys"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("organization_member_id") REFERENCES "organization_members"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "api_keys" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"key_hash" text NOT NULL,
|
||||
"prefix" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"last_used_at" integer,
|
||||
"expires_at" integer,
|
||||
"permissions" text DEFAULT '[]' NOT NULL,
|
||||
"all_organizations" integer DEFAULT false NOT NULL,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "api_keys_key_hash_unique" ON "api_keys" ("key_hash")`),
|
||||
db.run(sql`CREATE INDEX IF NOT EXISTS "key_hash_index" ON "api_keys" ("key_hash")`),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`DROP TABLE IF EXISTS "api_key_organizations"`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "api_keys"`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "api_keys_key_hash_unique"`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "key_hash_index"`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const organizationsWebhooksMigration = {
|
||||
name: 'organizations-webhooks',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "webhook_deliveries" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"webhook_id" text NOT NULL,
|
||||
"event_name" text NOT NULL,
|
||||
"request_payload" text NOT NULL,
|
||||
"response_payload" text NOT NULL,
|
||||
"response_status" integer NOT NULL,
|
||||
FOREIGN KEY ("webhook_id") REFERENCES "webhooks"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "webhook_events" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"webhook_id" text NOT NULL,
|
||||
"event_name" text NOT NULL,
|
||||
FOREIGN KEY ("webhook_id") REFERENCES "webhooks"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "webhook_events_webhook_id_event_name_unique" ON "webhook_events" ("webhook_id","event_name")`),
|
||||
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "webhooks" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"updated_at" integer NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"secret" text,
|
||||
"enabled" integer DEFAULT true NOT NULL,
|
||||
"created_by" text,
|
||||
"organization_id" text,
|
||||
FOREIGN KEY ("created_by") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null,
|
||||
FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
`),
|
||||
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`DROP TABLE IF EXISTS "webhook_deliveries"`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "webhook_events"`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "webhook_events_webhook_id_event_name_unique"`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "webhooks"`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const organizationsInvitationsImprovementMigration = {
|
||||
name: 'organizations-invitations-improvement',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`ALTER TABLE "organization_invitations" ALTER COLUMN "role" TO "role" text NOT NULL`),
|
||||
db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS "organization_invitations_organization_email_unique" ON "organization_invitations" ("organization_id","email")`),
|
||||
db.run(sql`ALTER TABLE "organization_invitations" ALTER COLUMN "status" TO "status" text NOT NULL DEFAULT 'pending'`),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`ALTER TABLE "organization_invitations" ALTER COLUMN "role" TO "role" text`),
|
||||
db.run(sql`DROP INDEX IF EXISTS "organization_invitations_organization_email_unique"`),
|
||||
db.run(sql`ALTER TABLE "organization_invitations" ALTER COLUMN "status" TO "status" text NOT NULL`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const documentActivityLogMigration = {
|
||||
name: 'document-activity-log',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS "document_activity_log" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"document_id" text NOT NULL,
|
||||
"event" text NOT NULL,
|
||||
"event_data" text,
|
||||
"user_id" text,
|
||||
"tag_id" text,
|
||||
FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE no action,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE no action
|
||||
);
|
||||
`),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`DROP TABLE IF EXISTS "document_activity_log"`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const documentActivityLogOnDeleteSetNullMigration = {
|
||||
name: 'document-activity-log-on-delete-set-null',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`PRAGMA foreign_keys=OFF`),
|
||||
db.run(sql`
|
||||
CREATE TABLE "__new_document_activity_log" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"document_id" text NOT NULL,
|
||||
"event" text NOT NULL,
|
||||
"event_data" text,
|
||||
"user_id" text,
|
||||
"tag_id" text,
|
||||
FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE set null
|
||||
);
|
||||
`),
|
||||
db.run(sql`
|
||||
INSERT INTO "__new_document_activity_log"("id", "created_at", "document_id", "event", "event_data", "user_id", "tag_id") SELECT "id", "created_at", "document_id", "event", "event_data", "user_id", "tag_id" FROM "document_activity_log";
|
||||
`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "document_activity_log"`),
|
||||
db.run(sql`ALTER TABLE "__new_document_activity_log" RENAME TO "document_activity_log"`),
|
||||
db.run(sql`PRAGMA foreign_keys=ON`),
|
||||
]);
|
||||
},
|
||||
|
||||
down: async ({ db }) => {
|
||||
await db.batch([
|
||||
db.run(sql`PRAGMA foreign_keys=OFF`),
|
||||
db.run(sql`
|
||||
CREATE TABLE "__restore_document_activity_log" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"created_at" integer NOT NULL,
|
||||
"document_id" text NOT NULL,
|
||||
"event" text NOT NULL,
|
||||
"event_data" text,
|
||||
"user_id" text,
|
||||
"tag_id" text,
|
||||
FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE no action,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE no action
|
||||
);
|
||||
`),
|
||||
db.run(sql`INSERT INTO "__restore_document_activity_log"("id", "created_at", "document_id", "event", "event_data", "user_id", "tag_id") SELECT "id", "created_at", "document_id", "event", "event_data", "user_id", "tag_id" FROM "document_activity_log";`),
|
||||
db.run(sql`DROP TABLE IF EXISTS "document_activity_log"`),
|
||||
db.run(sql`ALTER TABLE "__restore_document_activity_log" RENAME TO "document_activity_log"`),
|
||||
db.run(sql`PRAGMA foreign_keys=ON`),
|
||||
]);
|
||||
},
|
||||
} satisfies Migration;
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Migration } from '../migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const dropLegacyMigrationsMigration = {
|
||||
name: 'drop-legacy-migrations',
|
||||
description: 'Drop the legacy migrations table as it is not used anymore',
|
||||
|
||||
up: async ({ db }) => {
|
||||
await db.run(sql`DROP TABLE IF EXISTS "__drizzle_migrations"`);
|
||||
},
|
||||
|
||||
} satisfies Migration;
|
||||
1970
apps/papra-server/src/migrations/meta/0007_snapshot.json
Normal file
1970
apps/papra-server/src/migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,13 @@
|
||||
"when": 1748554484124,
|
||||
"tag": "0006_document-activity-log",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1754086182584,
|
||||
"tag": "0007_document-activity-log-on-delete-set-null",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
14
apps/papra-server/src/migrations/migration.tables.ts
Normal file
14
apps/papra-server/src/migrations/migration.tables.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const migrationsTable = sqliteTable(
|
||||
'migrations',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
runAt: integer('run_at', { mode: 'timestamp_ms' }).notNull().$default(() => new Date()),
|
||||
},
|
||||
t => [
|
||||
index('name_index').on(t.name),
|
||||
index('run_at_index').on(t.runAt),
|
||||
],
|
||||
);
|
||||
141
apps/papra-server/src/migrations/migrations.registry.test.ts
Normal file
141
apps/papra-server/src/migrations/migrations.registry.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { Migration } from './migrations.types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { setupDatabase } from '../modules/app/database/database';
|
||||
import { serializeSchema } from '../modules/app/database/database.test-utils';
|
||||
import { migrations } from './migrations.registry';
|
||||
import { rollbackLastAppliedMigration, runMigrations } from './migrations.usecases';
|
||||
|
||||
describe('migrations registry', () => {
|
||||
describe('migrations', () => {
|
||||
test('each migration should have a unique name', () => {
|
||||
const migrationNames = migrations.map(m => m.name);
|
||||
const duplicateMigrationNames = migrationNames.filter(name => migrationNames.filter(n => n === name).length > 1);
|
||||
|
||||
expect(duplicateMigrationNames).to.eql([], 'Each migration should have a unique name');
|
||||
});
|
||||
|
||||
test('each migration should have a non empty name', () => {
|
||||
const migrationNames = migrations.map(m => m.name);
|
||||
const emptyMigrationNames = migrationNames.filter(name => name === '');
|
||||
|
||||
expect(emptyMigrationNames).to.eql([], 'Each migration should have a non empty name');
|
||||
});
|
||||
|
||||
test('all migrations must be able to be applied without error and the database should be in a consistent state', async () => {
|
||||
const { db } = setupDatabase({ url: ':memory:' });
|
||||
|
||||
// This will throw if any migration is not able to be applied
|
||||
await runMigrations({ db, migrations });
|
||||
|
||||
// check foreign keys are enabled
|
||||
const { rows } = await db.run(sql`pragma foreign_keys;`);
|
||||
expect(rows).to.eql([{ foreign_keys: 1 }]);
|
||||
});
|
||||
|
||||
test('we can stop to any migration and still have a consistent database state', async () => {
|
||||
// Given like 3 migrations [A,B,C], creates [[A], [A,B], [A,B,C]]
|
||||
const migrationCombinations = migrations.map((m, i) => migrations.slice(0, i + 1));
|
||||
|
||||
for (const migrationCombination of migrationCombinations) {
|
||||
const { db } = setupDatabase({ url: ':memory:' });
|
||||
await runMigrations({ db, migrations: migrationCombination });
|
||||
}
|
||||
});
|
||||
|
||||
test('when we rollback to a previous migration, the database should be in the state of the previous migration', async () => {
|
||||
// Given like 3 migrations [A,B,C], creates [[A], [A,B], [A,B,C]]
|
||||
const migrationCombinations = migrations.map((m, i) => migrations.slice(0, i + 1));
|
||||
|
||||
for (const [index, migrationCombination] of migrationCombinations.entries()) {
|
||||
const { db } = setupDatabase({ url: ':memory:' });
|
||||
const previousMigration = migrationCombinations[index - 1] ?? [] as Migration[];
|
||||
|
||||
await runMigrations({ db, migrations: previousMigration });
|
||||
const previousDbState = await serializeSchema({ db });
|
||||
await runMigrations({ db, migrations: migrationCombination });
|
||||
await rollbackLastAppliedMigration({ db });
|
||||
|
||||
const currentDbState = await serializeSchema({ db });
|
||||
|
||||
expect(currentDbState).to.eql(previousDbState, `Downgrading from ${migrationCombination.at(-1)?.name ?? 'no migration'} should result in the same state as the previous migration`);
|
||||
}
|
||||
});
|
||||
|
||||
test('regression test of the database state after running migrations, update the snapshot when the database state changes', async () => {
|
||||
const { db } = setupDatabase({ url: ':memory:' });
|
||||
|
||||
await runMigrations({ db, migrations });
|
||||
|
||||
expect(await serializeSchema({ db })).toMatchInlineSnapshot(`
|
||||
"CREATE UNIQUE INDEX "api_keys_key_hash_unique" ON "api_keys" ("key_hash");
|
||||
CREATE INDEX "auth_sessions_token_index" ON "auth_sessions" ("token");
|
||||
CREATE INDEX "auth_verifications_identifier_index" ON "auth_verifications" ("identifier");
|
||||
CREATE INDEX "documents_organization_id_is_deleted_created_at_index" ON "documents" ("organization_id","is_deleted","created_at");
|
||||
CREATE INDEX "documents_organization_id_is_deleted_index" ON "documents" ("organization_id","is_deleted");
|
||||
CREATE UNIQUE INDEX "documents_organization_id_original_sha256_hash_unique" ON "documents" ("organization_id","original_sha256_hash");
|
||||
CREATE INDEX "documents_organization_id_size_index" ON "documents" ("organization_id","original_size");
|
||||
CREATE INDEX "documents_original_sha256_hash_index" ON "documents" ("original_sha256_hash");
|
||||
CREATE UNIQUE INDEX "intake_emails_email_address_unique" ON "intake_emails" ("email_address");
|
||||
CREATE INDEX "key_hash_index" ON "api_keys" ("key_hash");
|
||||
CREATE INDEX migrations_name_index ON migrations (name);
|
||||
CREATE INDEX migrations_run_at_index ON migrations (run_at);
|
||||
CREATE UNIQUE INDEX "organization_invitations_organization_email_unique" ON "organization_invitations" ("organization_id","email");
|
||||
CREATE UNIQUE INDEX "organization_members_user_organization_unique" ON "organization_members" ("organization_id","user_id");
|
||||
CREATE UNIQUE INDEX "tags_organization_id_name_unique" ON "tags" ("organization_id","name");
|
||||
CREATE INDEX "user_roles_role_index" ON "user_roles" ("role");
|
||||
CREATE UNIQUE INDEX "user_roles_user_id_role_unique_index" ON "user_roles" ("user_id","role");
|
||||
CREATE INDEX "users_email_index" ON "users" ("email");
|
||||
CREATE UNIQUE INDEX "users_email_unique" ON "users" ("email");
|
||||
CREATE UNIQUE INDEX "webhook_events_webhook_id_event_name_unique" ON "webhook_events" ("webhook_id","event_name");
|
||||
CREATE TABLE "api_key_organizations" ( "api_key_id" text NOT NULL, "organization_member_id" text NOT NULL, FOREIGN KEY ("api_key_id") REFERENCES "api_keys"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("organization_member_id") REFERENCES "organization_members"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "api_keys" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "name" text NOT NULL, "key_hash" text NOT NULL, "prefix" text NOT NULL, "user_id" text NOT NULL, "last_used_at" integer, "expires_at" integer, "permissions" text DEFAULT '[]' NOT NULL, "all_organizations" integer DEFAULT false NOT NULL, FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "auth_accounts" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "user_id" text, "account_id" text NOT NULL, "provider_id" text NOT NULL, "access_token" text, "refresh_token" text, "access_token_expires_at" integer, "refresh_token_expires_at" integer, "scope" text, "id_token" text, "password" text, FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "auth_sessions" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "token" text NOT NULL, "user_id" text, "expires_at" integer NOT NULL, "ip_address" text, "user_agent" text, "active_organization_id" text, FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("active_organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE set null );
|
||||
CREATE TABLE "auth_verifications" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "identifier" text NOT NULL, "value" text NOT NULL, "expires_at" integer NOT NULL );
|
||||
CREATE TABLE "document_activity_log" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "document_id" text NOT NULL, "event" text NOT NULL, "event_data" text, "user_id" text, "tag_id" text, FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null, FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE set null );
|
||||
CREATE TABLE "documents" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "is_deleted" integer DEFAULT false NOT NULL, "deleted_at" integer, "organization_id" text NOT NULL, "created_by" text, "deleted_by" text, "original_name" text NOT NULL, "original_size" integer DEFAULT 0 NOT NULL, "original_storage_key" text NOT NULL, "original_sha256_hash" text NOT NULL, "name" text NOT NULL, "mime_type" text NOT NULL, "content" text DEFAULT '' NOT NULL, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("created_by") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null, FOREIGN KEY ("deleted_by") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null );
|
||||
CREATE VIRTUAL TABLE documents_fts USING fts5(id UNINDEXED, name, original_name, content, prefix='2 3 4');
|
||||
CREATE TABLE 'documents_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID;
|
||||
CREATE TABLE 'documents_fts_content'(id INTEGER PRIMARY KEY, c0, c1, c2, c3);
|
||||
CREATE TABLE 'documents_fts_data'(id INTEGER PRIMARY KEY, block BLOB);
|
||||
CREATE TABLE 'documents_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB);
|
||||
CREATE TABLE 'documents_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
|
||||
CREATE TABLE "documents_tags" ( "document_id" text NOT NULL, "tag_id" text NOT NULL, PRIMARY KEY("document_id", "tag_id"), FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "intake_emails" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "email_address" text NOT NULL, "organization_id" text NOT NULL, "allowed_origins" text DEFAULT '[]' NOT NULL, "is_enabled" integer DEFAULT true NOT NULL, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, run_at INTEGER NOT NULL);
|
||||
CREATE TABLE "organization_invitations" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "organization_id" text NOT NULL, "email" text NOT NULL, "role" text NOT NULL, "status" text NOT NULL DEFAULT 'pending', "expires_at" integer NOT NULL, "inviter_id" text NOT NULL, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("inviter_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "organization_members" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "organization_id" text NOT NULL, "user_id" text NOT NULL, "role" text NOT NULL, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "organization_subscriptions" ( "id" text PRIMARY KEY NOT NULL, "customer_id" text NOT NULL, "organization_id" text NOT NULL, "plan_id" text NOT NULL, "status" text NOT NULL, "seats_count" integer NOT NULL, "current_period_end" integer NOT NULL, "current_period_start" integer NOT NULL, "cancel_at_period_end" integer DEFAULT false NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "organizations" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "name" text NOT NULL, "customer_id" text );
|
||||
CREATE TABLE sqlite_sequence(name,seq);
|
||||
CREATE TABLE "tagging_rule_actions" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "tagging_rule_id" text NOT NULL, "tag_id" text NOT NULL, FOREIGN KEY ("tagging_rule_id") REFERENCES "tagging_rules"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "tagging_rule_conditions" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "tagging_rule_id" text NOT NULL, "field" text NOT NULL, "operator" text NOT NULL, "value" text NOT NULL, "is_case_sensitive" integer DEFAULT false NOT NULL, FOREIGN KEY ("tagging_rule_id") REFERENCES "tagging_rules"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "tagging_rules" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "organization_id" text NOT NULL, "name" text NOT NULL, "description" text, "enabled" integer DEFAULT true NOT NULL, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "tags" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "organization_id" text NOT NULL, "name" text NOT NULL, "color" text NOT NULL, "description" text, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "user_roles" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "user_id" text NOT NULL, "role" text NOT NULL, FOREIGN KEY ("user_id") REFERENCES "users"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "users" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "email" text NOT NULL, "email_verified" integer DEFAULT false NOT NULL, "name" text, "image" text, "max_organization_count" integer );
|
||||
CREATE TABLE "webhook_deliveries" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "webhook_id" text NOT NULL, "event_name" text NOT NULL, "request_payload" text NOT NULL, "response_payload" text NOT NULL, "response_status" integer NOT NULL, FOREIGN KEY ("webhook_id") REFERENCES "webhooks"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "webhook_events" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "webhook_id" text NOT NULL, "event_name" text NOT NULL, FOREIGN KEY ("webhook_id") REFERENCES "webhooks"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TABLE "webhooks" ( "id" text PRIMARY KEY NOT NULL, "created_at" integer NOT NULL, "updated_at" integer NOT NULL, "name" text NOT NULL, "url" text NOT NULL, "secret" text, "enabled" integer DEFAULT true NOT NULL, "created_by" text, "organization_id" text, FOREIGN KEY ("created_by") REFERENCES "users"("id") ON UPDATE cascade ON DELETE set null, FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON UPDATE cascade ON DELETE cascade );
|
||||
CREATE TRIGGER trigger_documents_fts_delete AFTER DELETE ON documents BEGIN DELETE FROM documents_fts WHERE id = old.id; END;
|
||||
CREATE TRIGGER trigger_documents_fts_insert AFTER INSERT ON documents BEGIN INSERT INTO documents_fts(id, name, original_name, content) VALUES (new.id, new.name, new.original_name, new.content); END;
|
||||
CREATE TRIGGER trigger_documents_fts_update AFTER UPDATE ON documents BEGIN UPDATE documents_fts SET name = new.name, original_name = new.original_name, content = new.content WHERE id = new.id; END;"
|
||||
`);
|
||||
});
|
||||
|
||||
// Maybe a bit fragile, but it's to try to enforce to have migrations fail-safe
|
||||
test('if for some reasons we drop the migrations table, we can reapply all migrations', async () => {
|
||||
const { db } = setupDatabase({ url: ':memory:' });
|
||||
|
||||
await runMigrations({ db, migrations });
|
||||
|
||||
const dbState = await serializeSchema({ db });
|
||||
|
||||
await db.run(sql`DROP TABLE migrations`);
|
||||
await runMigrations({ db, migrations });
|
||||
|
||||
expect(await serializeSchema({ db })).to.eq(dbState);
|
||||
});
|
||||
});
|
||||
});
|
||||
23
apps/papra-server/src/migrations/migrations.registry.ts
Normal file
23
apps/papra-server/src/migrations/migrations.registry.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Migration } from './migrations.types';
|
||||
|
||||
import { initialSchemaSetupMigration } from './list/0001-initial-schema-setup.migration';
|
||||
import { documentsFtsMigration } from './list/0002-documents-fts.migration';
|
||||
import { taggingRulesMigration } from './list/0003-tagging-rules.migration';
|
||||
import { apiKeysMigration } from './list/0004-api-keys.migration';
|
||||
import { organizationsWebhooksMigration } from './list/0005-organizations-webhooks.migration';
|
||||
import { organizationsInvitationsImprovementMigration } from './list/0006-organizations-invitations-improvement.migration';
|
||||
import { documentActivityLogMigration } from './list/0007-document-activity-log.migration';
|
||||
import { documentActivityLogOnDeleteSetNullMigration } from './list/0008-document-activity-log-on-delete-set-null.migration';
|
||||
import { dropLegacyMigrationsMigration } from './list/0009-drop-legacy-migrations.migration';
|
||||
|
||||
export const migrations: Migration[] = [
|
||||
initialSchemaSetupMigration,
|
||||
documentsFtsMigration,
|
||||
taggingRulesMigration,
|
||||
apiKeysMigration,
|
||||
organizationsWebhooksMigration,
|
||||
organizationsInvitationsImprovementMigration,
|
||||
documentActivityLogMigration,
|
||||
documentActivityLogOnDeleteSetNullMigration,
|
||||
dropLegacyMigrationsMigration,
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user